Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
Loading