Skip to content

Releases: sligara7/dndwright

dndwright 0.29.0

Choose a tag to compare

@sligara7 sligara7 released this 16 Aug 02:14

[0.29.0] — 2026-08-15

Changed — BREAKING

  • evaluate_character now defaults to strict=True. Malformed input raises
    CharacterInputError instead of being coerced into a plausible-but-wrong sheet.
    Pass strict=False to restore the previous behaviour — the lenient path is unchanged
    and still available, it is simply no longer the default.

    Why this is worth a breaking change. A coerced sheet has every expected key and
    plausible numbers: missing ability scores default to 10, an omitted level to 1, a missing
    class zeroes HP and spellcasting. It is therefore indistinguishable from a correct sheet
    at every downstream surface
    . There was no error to swallow and nothing looked broken, so
    no consumer could report it — the signal was never generated rather than hidden. A new
    test, test_the_coerced_sheet_is_indistinguishable_from_a_good_one, pins exactly that.

    What to check before upgrading. Anything calling evaluate_character without the flag
    on data it did not itself validate. Callers behind a schema gate (e.g. a Pydantic request
    model that already requires ability scores, class and level) are unaffected — the data is
    complete by construction. Callers that build input internally, especially from
    LLM output, are the ones that may newly raise.

    ⚠️ If you depend on this package with a >= floor rather than an exact pin, a rebuild
    will pick this up automatically.
    That is the intended behaviour of a floor, but it means
    the change can arrive without anyone editing a version file.

dndwright 0.28.0

Choose a tag to compare

@sligara7 sligara7 released this 16 Aug 00:48

[0.28.0] — 2026-08-15

Added

  • ClassMechanics and weapon_attack are now part of the public API (dndwright.__all__,
    72 → 74 names). Both were already reachable — ClassMechanics only via the private
    dndwright.rules.components, and weapon_attack via the published dndwright.combat
    sub-API — and both are now importable directly from dndwright. Additive: no existing
    import path was removed, so nothing breaks.
    • The API contract test pins all three of its surfaces accordingly: the exported name set,
      weapon_attack's signature (every public free function must be pinned), and
      ClassMechanics's field list (every public pydantic model must be pinned).
    • A minor version bump is owed at the next release under SemVer, since this widens the
      public surface. Deliberately not bumped here: bumping without releasing is what left
      the sibling package mapwright with a 0.28.0 that looks shipped and never was.

Changed

  • Examples and the module docstring now teach published import paths only.
    examples/multiclass.py and the __init__ docstring imported ClassMechanics from
    dndwright.rules.components; examples/stat_diff.py imported compute_stat_diff from
    dndwright.rules.character_evaluator even though that name was already public. Since
    tests/test_examples.py executes every example, the package was effectively guaranteeing
    that private import paths keep working.

Added (2)

  • dndwright.combat now re-exports both submodules' full declared surfaces — the 20 names
    in conditions.__all__ and the 7 in initiative.__all__. combat.__all__ goes 22 → 49, so
    from dndwright.combat import attempt_save, order_initiative works instead of needing a
    second import from the submodule. Additive: dndwright.combat.conditions and
    .initiative remain importable exactly as before.
    • Both submodule surfaces were ALREADY contract-pinned (EXPECTED_CONDITIONS_PUBLIC,
      EXPECTED_INITIATIVE_PUBLIC); this publishes them one level up and pins the wider
      EXPECTED_COMBAT_PUBLIC to match.
    • The full submodule surfaces were taken rather than only the nine names the examples
      happened to use, because four of those nine return types (SaveResult,
      ConditionChange, TurnAdvance, InitiativeRoll) would otherwise have stayed
      unpublished — leaving callers able to invoke a function but unable to name its result.
  • examples/conditions.py and examples/initiative.py now import from dndwright.combat.
    No example imports a private path any more.

Known

  • examples/conditions.py and examples/initiative.py still import names that are on no
    published surface
    — attempt_save, tick_conditions, ActiveCondition, ROUND_END,
    SAVE_ENDS, order_initiative, advance_turn, roll_initiative, InitiativeEntry.
    dndwright.combat.__all__ exports the conditions and initiative submodules but none
    of the functions within them. Widening the surface further is a separate decision and has
    not been taken.

Changed

  • Adopted the wider lint families (I, SIM, UP, RUF) on top of the existing
    explicit rule set, in a deliberate pass rather than as a dependency side effect:
    import sorting, f-string conversions, __all__ sorting, and a deprecated import.
    PL is not adopted (262 findings, magic values that are the ruleset itself);
    RUF001-003, RUF005 and RUF012 are declined with reasons stated in
    pyproject.toml.

Added

  • RUF046 is enabled here, and the reason is now checked. RUF046 rewrites
    int(round(x)) into round(x), which is only safe while no value is a numpy
    scalar — round(np.float64) returns a numpy float, not an int. dndwright has no
    numpy by rule, so the rewrite is safe; the sibling package mapwright is numpy-based
    and declines the same rule. Those two decisions are coupled and nothing said so, so
    tests/test_packaging_contract.py now fails if numpy is ever added while RUF046
    is still enforced, naming the reason.

Notes

  • No behavioural change and no version bump. The public API is the same set of 72
    names (__all__ declaration order moved when it was sorted; the set did not), and
    seeded dice rolls and a computed character sheet hash identically against a detached
    worktree of the previous commit.

dndwright 0.27.0

Choose a tag to compare

@sligara7 sligara7 released this 29 Jul 02:18

[0.27.0] — 2026-07-29

Added

  • clean_damage_types() and combatant_defenses() now accept an optional keyword-only
    allowed: frozenset[str] = DAMAGE_TYPES. By default they intersect with the SRD-13
    DAMAGE_TYPES exactly as before (fully backward-compatible); a caller that governs its own
    damage-type vocabulary can pass a wider set (e.g. SRD-13 ∪ custom) so caller-registered
    types survive the filter — at component assembly, the computed-stats read, and live combat
    damage application — instead of being silently dropped. dndwright stays domain-agnostic: it does
    not know where allowed comes from; members must be lower-cased (the caller owns canonicalisation).
    Enables host applications to support per-campaign/per-universe custom damage types without a
    split-brain between the character sheet and combat. Public API names (__all__) unchanged.

v0.26.0 — evaluate_character(components=...) compose

Choose a tag to compare

@sligara7 sligara7 released this 01 Jul 11:33
358eb3e

What's Changed

  • feat(rules): evaluate_character(components=...) — compose item modifiers onto the sheet by @sligara7 in #9

Full Changelog: v0.25.2...v0.26.0

v0.25.2

Choose a tag to compare

@sligara7 sligara7 released this 17 Jun 03:42

Full Changelog: v0.25.1...v0.25.2

dndwright 0.23.2

Choose a tag to compare

@sligara7 sligara7 released this 06 Jun 11:22
7896d37

Documentation

  • Two new README/PyPI diagrams. assets/compose.svg shows lego-style components snapping
    onto the ruleset — typed set/add/union contributions that keep the target node's id, so
    one snap-in recomputes the whole downstream subtree. assets/theme-scaling.svg shows one
    computation graph re-skinned per setting via ThemeScalingLayer: the same plate node emerges
    as AC 18 (traditional / modern, re-flavored) or AC 19 (sci-fi / steampunk) through
    input_overrides, lookup_overrides, and display-only flavor_renames.
  • New README sections — "Composable — snap mini-graphs onto the ruleset" and "Re-skin for
    any setting — theme scaling" (theme scaling was previously undocumented in the README), plus a
    apply_theme_scaling / ThemeScalingLayer row in the feature table. Docs-only; no code change.

dndwright 0.23.1

Choose a tag to compare

@sligara7 sligara7 released this 06 Jun 10:41
bc80114

Fixed

  • Critical damage preserves dice modifiers — DiceEngine.roll_damage(expr, is_critical=True)
    doubled die counts by rebuilding the expression as bare XdY, silently dropping per-group
    keep/drop/reroll/exploding flags. A crit on 1d6!+2 now stays exploding (2d6!+2)
    instead of becoming a plain 2d6+2. As part of the fix, str(DiceGroup) now serialises
    reroll-once as ro (was r), so the spec round-trips through parse_expression.
  • Species damage immunities mis-categorised — _build_species_traits tagged damage
    immunities with category: "resistance" (condition immunities were already correct); they
    are now category: "immunity".

Changed

  • compose() fails loudly on contradictory contributions — a target that receives both a
    union (set-valued) and an add/set (numeric) contribution now raises ValueError instead
    of silently dropping the numeric ones.

Performance

  • Topological sort uses a binary heap (heapq) instead of re-sorting the ready-queue every
    iteration — same deterministic order, O(V·log V + E) instead of O(V²·log V). (Result is
    still cached per ruleset, so this only matters the first time a large graph is evaluated.)

Internal

  • cli._read_json closes the file it opens (was leaked); cli content lookup calls
    categories() once instead of twice.
  • assembler.assemble_inputs copies the ability-score dict once up front rather than on each
    increase.

dndwright 0.23.0

Choose a tag to compare

@sligara7 sligara7 released this 06 Jun 01:18

[0.23.0] — 2026-06-05

Added

  • evaluate_character(..., scaling=...) — evaluate_character, compute_key_stats, and
    compute_stat_diff now take an optional scaling: ThemeScalingLayer keyword. When given,
    the sheet (or key-stat snapshot / before-after diff) is computed against the theme-scaled
    ruleset via apply_theme_scaling, so a campaign's setting (sci-fi, modern warfare, …) shapes
    the character's mechanical values. scaling=None (default) reproduces the stock 5e behaviour
    exactly — existing callers are unaffected. New tests/test_character_theme_scaling.py.

dndwright 0.22.0

Choose a tag to compare

@sligara7 sligara7 released this 06 Jun 00:45

[0.22.0] — 2026-06-05

Added

  • apply_theme_scaling(ruleset, layer) (dndwright) — folds a ThemeScalingLayer's
    mechanical overrides onto a ruleset so the same computation graph yields
    setting-appropriate values (modern warfare, sci-fi, steampunk, …). lookup_overrides
    deep-merge into the ruleset's lookup tables (creating tables/keys, preserving the rest);
    input_overrides re-baseline a node's default_value (eval precedence stays explicit
    input > themed default > original default
    , so a theme sets the world's baseline without
    clobbering a value a character explicitly carries). flavor_renames are display-only and
    ignored here. Pure (the base ruleset is untouched) and composes with compose(). Raises
    KeyError on an input_overrides id that names no node, to catch authored-layer typos.
    This makes the previously inert ThemeScalingLayer / PREDEFINED_THEME_SCALING data a
    working feature. New tests/test_theme_scaling.py.

v0.21.0 — content structure contract

Choose a tag to compare

@sligara7 sligara7 released this 04 Jun 04:23
a258482

Formalizes dndwright's D&D 5e content structure as exported, strict Pydantic models — the single source of truth that downstream services conform to.

  • New dndwright.content.models + exported Creature, CharClass, Species, Spell, MagicItem, Background, Feat, Weapon, Armor, Condition, Modifier, CONTENT_MODELS.
  • All 1012 bundled assets validate against their models (extra="forbid").
  • Non-breaking: asset shapes unchanged, only formalized.

See #2.