diff --git a/CHANGELOG.md b/CHANGELOG.md index d2ac76c..3e92cfe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), ### Added -- [`Adventure.party`][osrlib.crawl.adventure.Adventure] records the party an adventure is written for, as a [`PartySpec`][osrlib.crawl.adventure.PartySpec] with the lowest and highest character level and, when the adventure says, the fewest and most characters, the way a published module states it on its cover ("for 6 to 8 characters of levels 1 to 3"). It defaults `None`, so an existing adventure document still loads and no `schema_version` moves. The engine never reads it, so no rule, event, or draw sequence changes. A front end can use it to offer a party that fits, a converter can carry the cover's figures across, and a playtest can pick its characters by level. `PartySpec` is frozen, refuses a level or size below 1, and refuses a maximum below its minimum. - [`osrlib.core.creature`][osrlib.core.creature] declares the three protocols a rules function reads its creature argument through: [`Creature`][osrlib.core.creature.Creature] (id, name, alignment, hit points, conditions, stat modifiers), [`Combatant`][osrlib.core.creature.Combatant] (the attack, initiative, and saving-throw numbers), and [`Caster`][osrlib.core.creature.Caster] (level, spell book, memorized spells) (#104). A [`Character`][osrlib.core.character.Character] satisfies all three and a [`MonsterInstance`][osrlib.core.monsters.MonsterInstance] the first two, structurally, so a caller passes either without a cast and annotates its own functions with the protocol that names what they read. Every public creature parameter in [`osrlib.core.combat`][osrlib.core.combat], [`osrlib.core.spells`][osrlib.core.spells], [`osrlib.core.effects`][osrlib.core.effects], and [`osrlib.core.items`][osrlib.core.items] now states the protocol the function reads through it, in place of `Any` and `object`: [`attack_roll`][osrlib.core.combat.attack_roll] takes a `Combatant` attacker and defender, [`has_condition`][osrlib.core.effects.has_condition] a `Creature`, [`cast_spell`][osrlib.core.spells.cast_spell] a `Caster` and a sequence of `Creature` values or the location strings a place-targeted spell takes, and a function that reads what one concrete type alone has takes that type, so [`resolve_breath`][osrlib.core.combat.resolve_breath] takes a `MonsterInstance` and [`sword_control_check`][osrlib.core.items.sword_control_check] a `Character`. The signature is what pyright checks a front end's call against and what the reference links. Runtime behaviour is unchanged, no rule, event, or draw sequence moved, and what changes is what a type checker accepts: a caller that passed an object without the protocol's attributes now fails its own pyright run. [`GameSession.combatant`][osrlib.crawl.session.GameSession.combatant] returns `MonsterInstance | Character | None` rather than `object | None`, so a front end can hand its result straight to a rules function. - [`StreamName`][osrlib.core.rng.StreamName] is the one home for every RNG stream key the library draws from, and each public `*_STREAM` constant takes its value from the matching member, so `COMBAT_STREAM` and `StreamName.COMBAT` are one object (#106). Name a stream through the enum rather than writing its key out: a misspelled key raises nothing, because it forks a stream of its own and draws plausible numbers from it, which is the one defect the determinism contract cannot catch. `StreamName` is a `StrEnum`, so a member is its own string and the values are unchanged: [`RngStreams`][osrlib.core.rng.RngStreams] keys by string, a save file records the same keys, and seed material and draw order are untouched. - [`MoraleCheckedEvent`][osrlib.core.events.MoraleCheckedEvent]`.held` states whether the side keeps fighting, and [`check_morale`][osrlib.core.combat.check_morale] fills it on every code. `combat.morale.exempt` covers both exemptions, so a front end that wants to tell a side that never fights from one that never breaks reads `held` instead of pairing the code with the score. The default formatter's `combat.morale.exempt` line reads it, and falls back to the score for a log written before schema 4, where the field defaults `None`. @@ -43,6 +42,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), - A scroll read is judged at the scroll's own caster level, in the kernel and in the crawl layer alike (#117). [`cast_from_scroll`][osrlib.core.spells.cast_from_scroll] validates and resolves at the spell's minimum caster level, so the two things that scale with caster level follow the scroll rather than the reader: a *magic missile* scroll takes one target however high the reader's level, and a per-level range is measured from the scroll's reach. The kernel exports the pre-check itself as [`validate_scroll_cast`][osrlib.core.spells.validate_scroll_cast], which builds the scroll-level caster and returns the rejections `cast_from_scroll` would raise on; `cast_from_scroll` runs it, and so do [`UseItem`][osrlib.crawl.commands.UseItem] and the `use_item` battle declaration, so there is one statement of the rule and a front end deciding whether to offer a read can ask it too. The crawl therefore refuses exactly what the kernel would refuse, before any state change, draw, or event: a 6th-level magic-user reading a *magic missile* scroll is refused `magic.cast.target_count` for three targets and keeps the scroll. - [`generate_treasure`][osrlib.core.treasure.generate_treasure], [`generate_treasure_entries`][osrlib.core.treasure.generate_treasure_entries], and [`generate_unguarded_treasure`][osrlib.core.treasure.generate_unguarded_treasure] raise `ValueError` for a `tier` that is neither `"basic"` nor `"expert"` before their first draw, the check [`generate_magic_item`][osrlib.core.treasure.generate_magic_item] already makes. A bad tier costs no draws and yields no hoard, so a caller who passes a typo sees the bug rather than a plausible pile of coins and gems. A valid call's draw sequence is unchanged. +## [1.8.0] - 2026-10-04 + +### Added + +- [`Adventure.party`][osrlib.crawl.adventure.Adventure] records the party an adventure is written for, as a [`PartySpec`][osrlib.crawl.adventure.PartySpec] with the lowest and highest character level and, when the adventure says, the fewest and most characters, the way a published module states it on its cover ("for 6 to 8 characters of levels 1 to 3"). It defaults `None`, so an existing adventure document still loads and no `schema_version` moves. The engine never reads it, so no rule, event, or draw sequence changes. A front end can use it to offer a party that fits, a converter can carry the cover's figures across, and a playtest can pick its characters by level. `PartySpec` is frozen, refuses a level or size below 1, and refuses a maximum below its minimum. + ## [1.7.1] - 2026-08-23 ### Changed @@ -171,6 +176,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), - The typed surface: complete type hints under `py.typed`, checked in CI. [Unreleased]: https://github.com/mmacy/osrlib-python/compare/v1.7.1...HEAD +[1.8.0]: https://github.com/mmacy/osrlib-python/compare/v1.7.1...v1.8.0 [1.7.1]: https://github.com/mmacy/osrlib-python/compare/v1.7.0...v1.7.1 [1.7.0]: https://github.com/mmacy/osrlib-python/compare/v1.6.0...v1.7.0 [1.6.0]: https://github.com/mmacy/osrlib-python/compare/v1.5.0...v1.6.0