Annotate the kernel's creature parameters with the protocols (#104) - #142
Merged
Merged
Conversation
Every public creature parameter in core/combat.py, core/spells.py, core/effects.py, and core/items.py now names what the function reads through it, in place of Any and object. A parameter whose reads are the id, name, alignment, hit points, conditions, and stat modifiers takes Creature; one that reads THAC0, armour class, or a saving throw takes Combatant; one that reads level, spell book, or memorized spells takes Caster; and one whose reads need what a single concrete type has takes that type, so resolve_breath and drain_monster_hd take a MonsterInstance and sword_control_check a Character. A sequence parameter takes a Sequence of the same choice, and a spell's targets take Creature values or the location strings the module already documented. The point is that a front end's call is now checked from the signature and the reference links the type, instead of prose promising what the annotation did not say. The three protocols are unchanged: no function read, unguarded, an attribute both concrete types have that the protocols lack, so nothing was added to them. Runtime behaviour is identical. No branch, guard, or draw was added or moved: the only executable changes are imports, five typing.cast calls where a runtime gate the checker cannot see has already settled which concrete type is in hand, and _ScrollReader keeping the scroll's caster level in a slot rather than behind a read-only property, so the proxy satisfies Caster where it is passed as one and declares the members it forwards. The full suite passes with no golden file touched. pyright reports six errors, all in the crawl layer, which this chunk was told not to touch: battle.py declares slow_attacks with an object element type, two handlers in exploration.py declare their spell target list as list[object], and one healing path leaves its target Unknown | None across two separate tests of the same condition. Widening the kernel's Any to a protocol is what exposed them; the kernel itself is clean. Claude-Session: https://claude.ai/code/session_01NmCezTw8hKKujkaEZ3YGAs
Annotating the kernel's creature parameters turned three crawl-layer declarations into pyright errors, because each one launders a creature through object before handing it to a kernel function. battle.py's slow_attacks takes its element type from the party member it holds beside the declaration, and exploration.py's two spell target lists take theirs from what a cast accepts, a creature or the location string a place-targeted spell takes. _use_device's healing target is typed Any rather than Creature | None. Its value is resolved in one `effect_spec.kind == "healing"` block and used in a second block testing the same condition, which pyright cannot correlate, so the declared union stays None-tainted at the call into apply_healing and only Any clears it. Narrowing that properly means restructuring the two blocks, which is a change to the crawl layer this chunk has no business making. Nothing else in the crawl layer changed, no behaviour changed, and no golden file moved. pyright now reports zero errors. Claude-Session: https://claude.ai/code/session_01NmCezTw8hKKujkaEZ3YGAs
- The `osrlib.core.creature` module docstring claimed every rules function
takes one of the three protocols. Six functions take a concrete creature
type, which the paragraph below it already permits, so the opening sentence
now allows for it and points at the function's own entry.
- `validate_turn_undead` took its cleric as Creature while `turn_undead` took
the same argument as Caster, and the validator's docstring calls it a
character with a cleric's class. It takes Caster now.
- Five Args entries in combat.py said "a Combatant that a Character or a
MonsterInstance satisfies", which reads the wrong way round against the form
every other entry uses. They now say "which a Character and a MonsterInstance
both satisfy".
- The rule each cast encodes is stated where a caller reads it. `deal_damage`
says that only a monster instance has a regeneration ability, so a target
that reaches the non-regenerable ledger is one and `nonregen_damage` is the
field written, and that a monster instance records the round it was last
damaged when a clock is passed. `resolve_energy_drain` says the class
definition is what tells the two victims apart: a character loses experience
levels, a monster loses Hit Dice. The comment beside `cast("Character",
target)` now says why the type name is quoted, as items.py does.
No signature changed except the validator's, no behaviour changed, and no
golden file moved.
Claude-Session: https://claude.ai/code/session_01NmCezTw8hKKujkaEZ3YGAs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Every public creature parameter in
core/combat.py,core/spells.py,core/effects.py, andcore/items.pynow names what the function reads through it, in place ofAnyandobject.Creature:has_condition,apply_healing,incapacitated,alignments_differ,check_immunity,validate_attack,select_targets, and their kin.Combatant:attack_roll,resolve_attack,saving_throw,damage_roll,deal_damage(its destructive-kill path rolls the victim's saves),destroy_equipment,participant_modifier,melee_modifier_for, andresolve_splash_attack.Caster:memorize_spells,validate_cast,cast_spell,cast_from_scroll,validate_turn_undead,turn_undead, and the rest of the casting surface.MonsterInstanceforvalidate_breath,resolve_breath,drain_monster_hd,resolve_energy_drain's attacker, andturn_undead's candidates;Characterforsword_control_check, imported underTYPE_CHECKING.Sequenceof the same choice, and a spell'stargetstakesCreaturevalues or the location strings the module already documented for place-targeted spells.The three protocols are unchanged. No function reads, unguarded, an attribute both concrete types have that the protocols lack, so nothing was added to
osrlib.core.creature.Runtime behaviour is identical. No branch, guard, or draw was added or moved. The only executable changes are imports, six
typing.castcalls (three indeal_damage, three inresolve_energy_drain) where a runtime gate the type checker cannot see has already settled which concrete type is in hand, and_ScrollReaderkeeping the scroll's caster level in a slot rather than behind a read-only property, so the proxy satisfiesCasterwhere it is passed as one and declares the members it forwards. The rule behind each cast is stated in the function's own docstring. EveryArgsentry that promised aCharacteror aMonsterInstancein prose now links the protocol and says which concrete types satisfy it, and the module prose that described the parameters as duck-typed names the protocols instead.Three crawl-layer declarations are typed in the second commit, because each launders a creature through
objecton its way into a kernel function and pyright had nothing to check until the kernel said what it reads:battle.py'sslow_attackselement type, andexploration.py's two spell target lists._use_device's healing target is typedAnyrather thanCreature | None, since its value is resolved in oneeffect_spec.kind == "healing"block and used in a second block testing the same condition, which pyright cannot correlate; narrowing it properly means restructuring those blocks, which belongs to a crawl-scoped change rather than this one. Nothing else in the crawl layer changed.Gate, run in this worktree at
3d40cc6after mergingorigin/main:uv run pyrightreports0 errors, 0 warnings, 0 informations;uv run ruff format --checkreports 160 files already formatted;uv run ruff checkpasses;uv run pytestreports2935 passed, 146 skipped, 2 xpassedwith no golden file touched;uv run mkdocs build --strictbuilds clean.uv run pytest --runxfail tests/test_creature_protocols.pypasses all 7 tests. The two xpasses are this chunk's own markers, left in place for the lead to remove.Closes #104
https://claude.ai/code/session_01NmCezTw8hKKujkaEZ3YGAs