Repository navigation
Releases: sligara7/dndwright
Release list
dndwright 0.29.0
[0.29.0] — 2026-08-15
Changed — BREAKING
-
evaluate_characternow defaults tostrict=True. Malformed input raises
CharacterInputErrorinstead of being coerced into a plausible-but-wrong sheet.
Passstrict=Falseto 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_characterwithout 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
[0.28.0] — 2026-08-15
Added
ClassMechanicsandweapon_attackare now part of the public API (dndwright.__all__,
72 → 74 names). Both were already reachable —ClassMechanicsonly via the private
dndwright.rules.components, andweapon_attackvia the publisheddndwright.combat
sub-API — and both are now importable directly fromdndwright. 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.
- The API contract test pins all three of its surfaces accordingly: the exported name set,
Changed
- Examples and the module docstring now teach published import paths only.
examples/multiclass.pyand the__init__docstring importedClassMechanicsfrom
dndwright.rules.components;examples/stat_diff.pyimportedcompute_stat_difffrom
dndwright.rules.character_evaluatoreven though that name was already public. Since
tests/test_examples.pyexecutes every example, the package was effectively guaranteeing
that private import paths keep working.
Added (2)
dndwright.combatnow re-exports both submodules' full declared surfaces — the 20 names
inconditions.__all__and the 7 ininitiative.__all__.combat.__all__goes 22 → 49, so
from dndwright.combat import attempt_save, order_initiativeworks instead of needing a
second import from the submodule. Additive:dndwright.combat.conditionsand
.initiativeremain 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_PUBLICto 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.
- Both submodule surfaces were ALREADY contract-pinned (
examples/conditions.pyandexamples/initiative.pynow import fromdndwright.combat.
No example imports a private path any more.
Known
examples/conditions.pyandexamples/initiative.pystill 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 theconditionsandinitiativesubmodules 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.
PLis not adopted (262 findings, magic values that are the ruleset itself);
RUF001-003,RUF005andRUF012are declined with reasons stated in
pyproject.toml.
Added
RUF046is enabled here, and the reason is now checked.RUF046rewrites
int(round(x))intoround(x), which is only safe while no value is a numpy
scalar —round(np.float64)returns a numpy float, not anint. 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.pynow fails if numpy is ever added whileRUF046
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
[0.27.0] — 2026-07-29
Added
clean_damage_types()andcombatant_defenses()now accept an optional keyword-only
allowed: frozenset[str] = DAMAGE_TYPES. By default they intersect with the SRD-13
DAMAGE_TYPESexactly 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 whereallowedcomes 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
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
dndwright 0.23.2
Documentation
- Two new README/PyPI diagrams.
assets/compose.svgshows lego-style components snapping
onto the ruleset — typedset/add/unioncontributions that keep the target node's id, so
one snap-in recomputes the whole downstream subtree.assets/theme-scaling.svgshows one
computation graph re-skinned per setting viaThemeScalingLayer: the sameplatenode emerges
as AC 18 (traditional / modern, re-flavored) or AC 19 (sci-fi / steampunk) through
input_overrides,lookup_overrides, and display-onlyflavor_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/ThemeScalingLayerrow in the feature table. Docs-only; no code change.
dndwright 0.23.1
Fixed
- Critical damage preserves dice modifiers —
DiceEngine.roll_damage(expr, is_critical=True)
doubled die counts by rebuilding the expression as bareXdY, silently dropping per-group
keep/drop/reroll/explodingflags. A crit on1d6!+2now stays exploding (2d6!+2)
instead of becoming a plain2d6+2. As part of the fix,str(DiceGroup)now serialises
reroll-once asro(wasr), so the spec round-trips throughparse_expression. - Species damage immunities mis-categorised —
_build_species_traitstagged damage
immunities withcategory: "resistance"(condition immunities were already correct); they
are nowcategory: "immunity".
Changed
compose()fails loudly on contradictory contributions — a target that receives both a
union(set-valued) and anadd/set(numeric) contribution now raisesValueErrorinstead
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 ofO(V²·log V). (Result is
still cached per ruleset, so this only matters the first time a large graph is evaluated.)
Internal
cli._read_jsoncloses the file it opens (was leaked);clicontent lookup calls
categories()once instead of twice.assembler.assemble_inputscopies the ability-score dict once up front rather than on each
increase.
dndwright 0.23.0
[0.23.0] — 2026-06-05
Added
evaluate_character(..., scaling=...)—evaluate_character,compute_key_stats, and
compute_stat_diffnow take an optionalscaling: ThemeScalingLayerkeyword. When given,
the sheet (or key-stat snapshot / before-after diff) is computed against the theme-scaled
ruleset viaapply_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. Newtests/test_character_theme_scaling.py.
dndwright 0.22.0
[0.22.0] — 2026-06-05
Added
apply_theme_scaling(ruleset, layer)(dndwright) — folds aThemeScalingLayer'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_overridesre-baseline a node'sdefault_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_renamesare display-only and
ignored here. Pure (the base ruleset is untouched) and composes withcompose(). Raises
KeyErroron aninput_overridesid that names no node, to catch authored-layer typos.
This makes the previously inertThemeScalingLayer/PREDEFINED_THEME_SCALINGdata a
working feature. Newtests/test_theme_scaling.py.
v0.21.0 — content structure contract
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+ exportedCreature,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.