Skip to content

osrlib.core.spells

Spell memorization, casting, spell resolution, and turning undead.

Part of the core kernel: everything here runs standalone — no game session required — and every random draw comes from a named, seeded RNG stream.

The OSE SRD's spell pages compile into a catalog of frozen SpellTemplate models loaded by load_spells. A template carries the page's presentation data (duration, range, prose) alongside structured mechanics: one SpellMode per castable usage, each naming its targeting, saving throw, and — for the automated subset — a SpellEffect the casting interpreter executes. Modes the kernel doesn't automate ship manual=True with the SRD prose: casting one is a supported operation (the slot is consumed, the event is emitted), and the game or narrator resolves the fiction.

The daily flow: memorize_spells prepares a caster's list (arcane casters choose from a spell book grown with add_spell_to_book), then validate_cast checks legality and cast_spell consumes the memorized copy and resolves the mode. cast_from_scroll resolves an inscribed spell without a memorized copy, and disrupt_casting loses one when a declared cast is broken. Clerics also turn undead here: validate_turn_undead, then turn_undead.

Casters are Character objects; targets arrive duck-typed per the combatant convention (see osrlib.core.combat) as characters, MonsterInstance objects, or location strings for effects a game attaches to places.

Reversed forms are entry data, not separate catalog entries: the nine concepts printed as separate cleric and magic-user pages compile as two entries with _c/_mu id suffixes because the pairs differ mechanically, while a reversible spell's reverse lives on its entry as a ReversedForm.

Every draw inside spell resolution — targeting dice, damage dice, touch-attack rolls, cast-time forced saves, dispel survival rolls, and both turning rolls — comes from the MAGIC_STREAM stream, so spell results replay independently of combat draws and vice versa. Effect-internal draws (rolled durations at attach, tick-time saves such as the charm re-save) stay on the EFFECTS_STREAM stream.

EFFECT_KINDS module-attribute

EFFECT_KINDS = frozenset(
    {"damage", "heal", "cure", "condition", "modifiers", "kill", "restore_life", "dispel", "attach_only"}
)

The closed vocabulary of effect kinds the casting interpreter executes.

MAGIC_STREAM module-attribute

MAGIC_STREAM = 'magic'

Stream key convention for spell-resolution draws: targeting, damage, cast-time saves, turning.

CastContext

Bases: BaseModel

The caller-asserted situation a cast resolves under — the RAW referee surface.

in_combat gates the touch-attack roll ("In combat, a melee attack roll is required"; outside combat the touch lands without a roll). bound/gagged are the OSE SRD's freedom restraints, which the kernel has no model for. rounds_since_death and days_since_death are the caller's attestations for neutralize poison and raise dead (the session supplies them from its death records) — the kernel has no cause-of-death model, so supplying rounds_since_death is the attestation that the target died of poison; omit it for any other death. strength_tiers maps entity ids to "augmented"/"giant" for web's faster escape tiers (caller-asserted until such effects exist).

in_combat class-attribute instance-attribute

in_combat: bool = False

distance_feet class-attribute instance-attribute

distance_feet: int | None = None

bound class-attribute instance-attribute

bound: bool = False

gagged class-attribute instance-attribute

gagged: bool = False

rounds_since_death class-attribute instance-attribute

rounds_since_death: int | None = None

days_since_death class-attribute instance-attribute

days_since_death: int | None = None

strength_tiers class-attribute instance-attribute

strength_tiers: dict[str, str] = {}

CastResult

Bases: BaseModel

A cast's outcome: the consumed copy, what it affected, and the events.

manual=True means the kernel did the bookkeeping (the copy is spent, the cast event is emitted) and the game narrates the effect — prose carries the mode's SRD text for that. no_effect marks a resolved cast that affected nothing (an ineligible target, every save passed on a negating spell): the copy is still spent, never refunded — validation rejections are free, and refunding a resolved cast would leak hidden state such as an unseen target's immunity.

spell_id instance-attribute

spell_id: str

mode instance-attribute

mode: str

reversed class-attribute instance-attribute

reversed: bool = False

manual class-attribute instance-attribute

manual: bool = False

no_effect class-attribute instance-attribute

no_effect: bool = False

prose class-attribute instance-attribute

prose: str = ''

affected_ids class-attribute instance-attribute

affected_ids: tuple[str, ...] = ()

events class-attribute instance-attribute

events: tuple[Event, ...] = ()

CasterProfile

Bases: BaseModel

A class's casting nature, read from its divine_magic/arcane_magic tag.

kind instance-attribute

kind: Literal['divine', 'arcane']

spell_list instance-attribute

spell_list: str

DurationSpec

Bases: BaseModel

A parsed spell duration.

kind="fixed" durations are amount (or dice, rolled at attach) counts of unit, plus per_level extra units per caster level (the additive bonus: light (MU) is 6 turns +1 per level; hold person (MU)'s 1 turn per level is amount 0, per_level 1). concentration durations may carry a cap (Concentration (up to 1 day)). Anything unparseable keeps kind="special" with the raw string on the template — the parser never fails on prose.

kind instance-attribute

kind: Literal['instant', 'permanent', 'concentration', 'fixed', 'special']

unit class-attribute instance-attribute

unit: TimeUnit | None = None

amount class-attribute instance-attribute

amount: int | None = None

dice class-attribute instance-attribute

dice: str | None = None

per_level class-attribute instance-attribute

per_level: int = 0

concentration_cap_unit class-attribute instance-attribute

concentration_cap_unit: TimeUnit | None = None

concentration_cap_amount class-attribute instance-attribute

concentration_cap_amount: int | None = None

MemorizationResult

Bases: BaseModel

The outcome of a preparation: rejections, or the memorized event.

rejections class-attribute instance-attribute

rejections: tuple[Rejection, ...] = ()

events class-attribute instance-attribute

events: tuple[Event, ...] = ()

accepted property

accepted: bool

Whether the preparation was applied.

MemorizedSpell

Bases: BaseModel

One memorized copy of a spell: the id and the form fixed at memorization.

spell_id is a spell id from load_spells — see the spell id index. Arcane casters fix the normal or reversed form when memorizing (the OSE SRD: "The normal or reversed form of a spell must be selected when the spell is memorized"); divine casters always memorize the normal form and choose at cast time, so their copies carry reversed=False.

spell_id class-attribute instance-attribute

spell_id: str = Field(min_length=1)

reversed class-attribute instance-attribute

reversed: bool = False

RangeSpec

Bases: BaseModel

A parsed spell range.

feet carries the distance in feet for feet, yards (converted: 240 yards around the caster is 720), and per_level kinds; per_level_feet is the extra feet per caster level (cloudkill-style 60' +10' per level is feet 60, per_level_feet 10). touch covers The caster or a creature touched — self-targeting is allowed; presence forms and other prose are special.

kind instance-attribute

kind: Literal['caster', 'touch', 'feet', 'yards', 'per_level', 'special']

feet class-attribute instance-attribute

feet: int | None = None

per_level_feet class-attribute instance-attribute

per_level_feet: int | None = None

ReversedForm

Bases: BaseModel

A reversible spell's reversed version — entry data, never a separate entry.

duration_spec overrides the normal form's duration when the page prints a dual form (Instant / Permanent (curse) splits across the two); None means the reverse shares the normal form's duration.

name class-attribute instance-attribute

name: str = Field(min_length=1)

prose class-attribute instance-attribute

prose: str = ''

modes class-attribute instance-attribute

modes: tuple[SpellMode, ...] = Field(min_length=1)

duration class-attribute instance-attribute

duration: str | None = None

duration_spec class-attribute instance-attribute

duration_spec: DurationSpec | None = None

SaveSpec

Bases: BaseModel

One mode's saving throw: the category, a modifier, and what a pass means.

modifier is the target's adjustment (hold person's single-target −2, feeblemind's −4). on_save="negates" means a passed save avoids the effect; "half" halves damage, rounding down.

category instance-attribute

category: SaveCategory

modifier class-attribute instance-attribute

modifier: int = 0

on_save class-attribute instance-attribute

on_save: Literal['negates', 'half'] = 'negates'

SpellBookResult

Bases: BaseModel

The outcome of a spell-book addition: rejections, or the book event.

rejections class-attribute instance-attribute

rejections: tuple[Rejection, ...] = ()

events class-attribute instance-attribute

events: tuple[Event, ...] = ()

accepted property

accepted: bool

Whether the spell was added.

SpellCatalog

Bases: BaseModel

The loaded spell list, with id lookup and per-list filtering.

spells instance-attribute

spells: tuple[SpellTemplate, ...]

get

get(spell_id: str) -> SpellTemplate

Return the spell template for spell_id.

Parameters:

Name Type Description Default
spell_id str

The spell id, e.g. "fire_ball" or "hold_person_c" — see the spell id index.

required

Returns:

Type Description
SpellTemplate

The spell template.

Raises:

Type Description
ValueError

If no spell has that id.

by_list

by_list(spell_list: str, level: int | None = None) -> tuple[SpellTemplate, ...]

Return the spells on a class's list, optionally at one spell level.

Parameters:

Name Type Description Default
spell_list str

The list id, e.g. "cleric" or "magic_user".

required
level int | None

A spell level to filter by, or None for the whole list.

None

Returns:

Type Description
tuple[SpellTemplate, ...]

The matching templates, in catalog (id) order.

SpellEffect

Bases: BaseModel

The structured effect a mode's resolution executes — the closed vocabulary.

kind names the interpreter behavior; params carries the per-spell scalars (damage dice, per-level scaling, exclusions, revival windows). condition is the condition a condition effect attaches; cures_conditions and cures_effect_kinds name what a cure effect releases; modifiers is the stat-modifier bundle a modifiers effect grants through the effects engine.

kind instance-attribute

kind: str

condition class-attribute instance-attribute

condition: Condition | None = None

cures_conditions class-attribute instance-attribute

cures_conditions: tuple[Condition, ...] = ()

cures_effect_kinds class-attribute instance-attribute

cures_effect_kinds: tuple[str, ...] = ()

modifiers class-attribute instance-attribute

modifiers: tuple[ModifierSpec, ...] = ()

params class-attribute instance-attribute

params: dict[str, int | str | bool | tuple[int | str, ...]] = {}

SpellMode

Bases: BaseModel

One castable usage of a spell.

Multi-usage pages (cure light wounds, light) carry one mode per numbered usage. key is stable snake_case within the spell's form — casting names the mode by it. manual=True marks modes the kernel doesn't execute: casting one consumes the memorized copy and emits the cast event with the manual marker plus the prose, and the game or narrator resolves the fiction. Manual modes may omit targeting (the page carries no structured targeting); automated modes always carry targeting and an effect.

key class-attribute instance-attribute

key: str = Field(min_length=1)

targeting class-attribute instance-attribute

targeting: TargetingSpec | None = None

save class-attribute instance-attribute

save: SaveSpec | None = None

effect class-attribute instance-attribute

effect: SpellEffect | None = None

manual class-attribute instance-attribute

manual: bool = False

prose class-attribute instance-attribute

prose: str = ''

SpellTemplate

Bases: BaseModel

A spell, compiled from its SRD page.

Frozen SRD data: play never mutates a spell template. id is the slugified primary name (fire_ball, cure_light_wounds), with _c/_mu suffixes for the nine dual-page concepts. spell_list is an open, validated list id matched against CasterProfile.spell_list — the Classic catalog carries cleric and magic_user, and Advanced lists are additive data. duration and range keep the printed strings; the specs are the parsed forms. conjured_monsters embeds stat blocks printed on the page (sticks to snakes' snake, validated as a full monster template); conjured_monster_ids references existing monsters.json entries (conjure elemental's four 16-HD elementals).

id class-attribute instance-attribute

id: str = Field(min_length=1)

name class-attribute instance-attribute

name: str = Field(min_length=1)

spell_list class-attribute instance-attribute

spell_list: str = Field(pattern='^[a-z][a-z0-9_]*$')

level class-attribute instance-attribute

level: int = Field(ge=1, le=6)

duration class-attribute instance-attribute

duration: str = Field(min_length=1)

duration_spec instance-attribute

duration_spec: DurationSpec

range class-attribute instance-attribute

range: str = Field(min_length=1)

range_spec instance-attribute

range_spec: RangeSpec

reversed_form class-attribute instance-attribute

reversed_form: ReversedForm | None = None

modes class-attribute instance-attribute

modes: tuple[SpellMode, ...] = Field(min_length=1)

intro class-attribute instance-attribute

intro: str = ''

conjured_monsters class-attribute instance-attribute

conjured_monsters: tuple[MonsterTemplate, ...] = ()

conjured_monster_ids class-attribute instance-attribute

conjured_monster_ids: tuple[str, ...] = ()

overrides_applied class-attribute instance-attribute

overrides_applied: tuple[str, ...] = ()

mode

mode(key: str, *, reversed: bool = False) -> SpellMode

Return the mode with key on the normal or reversed form.

Parameters:

Name Type Description Default
key str

The mode key, e.g. "damage" or "blind".

required
reversed bool

True to look on the reversed form.

False

Returns:

Type Description
SpellMode

The mode.

Raises:

Type Description
ValueError

If the form or the key doesn't exist.

TargetingSpec

Bases: BaseModel

One mode's targeting: the shared combat targeting mode plus its parameters.

count/count_dice size up_to_n modes (hold person's group mode is 1d4); hd_budget_dice sizes hd_budget modes (sleep's 2d8); hd_cap bounds eligibility by Hit Dice (sleep mode 2's "4 HD or less", charm monster's "3 HD or less"); hd_min bounds it from below (charm monster's single mode takes "more than 3 HD"). Area shape and dimensions ship as structured data now; the battle machine maps its range-track geometry onto candidates.

mode instance-attribute

count class-attribute instance-attribute

count: int | None = None

count_dice class-attribute instance-attribute

count_dice: str | None = None

hd_budget_dice class-attribute instance-attribute

hd_budget_dice: str | None = None

hd_cap class-attribute instance-attribute

hd_cap: int | None = None

hd_min class-attribute instance-attribute

hd_min: int | None = None

shape class-attribute instance-attribute

shape: str | None = None

dimensions class-attribute instance-attribute

dimensions: dict[str, int] = {}

TurnUndeadResult

Bases: BaseModel

A turning attempt's outcome: the rolls, per-type verdicts, and who was affected.

roll instance-attribute

roll: int

hd_pool class-attribute instance-attribute

hd_pool: int | None = None

outcomes class-attribute instance-attribute

outcomes: tuple[TurningTypeOutcome, ...] = ()

affected_ids class-attribute instance-attribute

affected_ids: tuple[str, ...] = ()

destroyed_ids class-attribute instance-attribute

destroyed_ids: tuple[str, ...] = ()

events class-attribute instance-attribute

events: tuple[Event, ...] = ()

add_spell_to_book

add_spell_to_book(caster: Any, definition: ClassDefinition, catalog: SpellCatalog, spell_id: str) -> SpellBookResult

Add a spell to an arcane caster's book — the referee-level growth surface.

Covers mentoring and leveling; the fiction around it (the mentor's week, a lost book's rewriting costs) belongs to the game. The book holds, per spell level, at most the caster's current slot count at that level (the RAW "contains exactly the number of spells that the character is capable of memorizing", read per level). The book never auto-shrinks — it is a physical object; a drained character may hold a book over capacity and simply cannot add more until capacity catches up.

Parameters:

Name Type Description Default
caster Any

The learning caster: a Character with an arcane class; its spell_book tuple grows.

required
definition ClassDefinition

The caster's ClassDefinition.

required
catalog SpellCatalog

The loaded spell catalog, from load_spells.

required
spell_id str

The spell id to add — see the spell id index.

required

Returns:

Type Description
SpellBookResult

The rejections (nothing mutated) or the book-updated event.

cast_from_scroll

cast_from_scroll(
    reader: Any,
    spell: SpellTemplate,
    mode: str,
    *,
    reversed: bool = False,
    targets: Sequence[object] = (),
    context: CastContext | None = None,
    ledger: EffectsLedger,
    clock: GameClock,
    allocator: Any,
    registry: dict[str, Any],
    ruleset: Ruleset,
    stream: RngStream,
    effects_stream: RngStream
) -> CastResult

Cast an inscribed spell from a scroll: the scroll is the copy, and it burns.

Reuses validate_cast's legality gates and the full mode-resolution and effects machinery, but skips the memorized-copy consume — "when a scroll is read, the words disappear", so the caller marks the inscribed spell spent. The spell resolves at the minimum class level able to cast it (the least-power reading — see minimum_caster_level). Class-list gating (arcane readers for arcane scrolls, the thief's scroll-use ability) and the light requirement are the crawl's validation; the kernel resolves a legal read.

Parameters:

Name Type Description Default
reader Any

The reading character: a Character. Conditions and modifiers from the resolution land on the reader; only its effective level is proxied.

required
spell SpellTemplate

The inscribed SpellTemplate.

required
mode str

The mode key on the chosen form (see SpellTemplate.mode).

required
reversed bool

True to cast the reversed form (divine readers choose at cast).

False
targets Sequence[object]

The explicit target list, in the caller's order, per the combatant convention (see osrlib.core.combat): Character or MonsterInstance objects, or location strings.

()
context CastContext | None

The caller-asserted CastContext.

None
ledger EffectsLedger

The EffectsLedger durations ride.

required
clock GameClock

The GameClock.

required
allocator Any

The id allocator for attached effects: an IdAllocator.

required
registry dict[str, Any]

Live combatants by entity id — Character and MonsterInstance objects the resolution may mutate.

required
ruleset Ruleset

The Ruleset in play.

required
stream RngStream

The magic stream (the "magic" RngStream).

required
effects_stream RngStream

The effects stream, for attach-time dice.

required

Returns:

Type Description
CastResult

The cast outcome with its events.

Raises:

Type Description
ValueError

If the cast is invalid — validate with validate_cast (profile=None) first.

cast_spell

cast_spell(
    caster: Any,
    spell: SpellTemplate,
    mode: str,
    *,
    profile: CasterProfile,
    reversed: bool = False,
    targets: Sequence[object] = (),
    context: CastContext | None = None,
    ledger: EffectsLedger,
    clock: GameClock,
    allocator: Any,
    registry: dict[str, Any],
    ruleset: Ruleset,
    stream: RngStream,
    effects_stream: RngStream
) -> CastResult

Cast a memorized spell: consume the copy, resolve the mode, return the events.

Consumes the first matching memorized copy (lowest tuple index) whether or not the resolution ends up affecting anything — including a touch attack that misses (nothing in RAW holds the charge). Divine casters consume any copy of the spell and choose the form at cast time; arcane casters fixed the form at memorization. Casting anything releases the caster's own invisibility (RAW: attacking or casting breaks it). Manual modes emit the cast event with the manual marker and the kernel stops there — the game or narrator resolves the fiction.

Spell-resolution draws (targeting dice, damage dice, touch-attack rolls, cast-time forced saves, dispel survival rolls) come from stream — the MAGIC_STREAM convention; attach-time draws (rolled durations, web escape dice) come from effects_stream per the effects-engine convention, so the two subsystems replay independently.

Parameters:

Name Type Description Default
caster Any

The casting caster: a Character with a matching memorized copy; its memorized_spells tuple shrinks by one copy.

required
spell SpellTemplate

The SpellTemplate to cast, from the catalog load_spells returns.

required
mode str

The mode key on the chosen form (see SpellTemplate.mode).

required
profile CasterProfile

The caster's CasterProfile, from caster_profile on the definition the caller holds.

required
reversed bool

True to cast the reversed form.

False
targets Sequence[object]

The explicit target list, in the caller's order, per the combatant convention (see osrlib.core.combat): Character or MonsterInstance objects, or location strings for effects games attach to places.

()
context CastContext | None

The caller-asserted CastContext.

None
ledger EffectsLedger

The EffectsLedger durations ride.

required
clock GameClock

The GameClock.

required
allocator Any

The id allocator for attached effects: an IdAllocator.

required
registry dict[str, Any]

Live combatants by entity id — Character and MonsterInstance objects the resolution may mutate.

required
ruleset Ruleset

The Ruleset in play.

required
stream RngStream

The magic stream (the "magic" RngStream).

required
effects_stream RngStream

The effects stream, for attach-time dice.

required

Returns:

Type Description
CastResult

The cast outcome with its events.

Raises:

Type Description
ValueError

If the cast is invalid — validate with validate_cast first; casting an unmemorized spell is programmer misuse.

Examples:

from osrlib.core.alignment import Alignment
from osrlib.core.character import CHARACTER_CREATION_STREAM, create_character
from osrlib.core.clock import GameClock
from osrlib.core.effects import EFFECTS_STREAM, EffectsLedger
from osrlib.core.monsters import MONSTER_SPAWN_STREAM, IdAllocator, spawn_monster
from osrlib.core.rng import RngStreams
from osrlib.core.ruleset import Ruleset
from osrlib.core.spells import MAGIC_STREAM, MemorizedSpell, cast_spell, caster_profile, memorize_spells
from osrlib.data import load_classes, load_monsters, load_spells

rules = Ruleset()
streams = RngStreams(master_seed=11)
catalog = load_spells()
definition = load_classes().get("magic_user")

# A 1st-level magic-user with *magic missile* in her book, memorized for the day.
created = create_character(
    name="Zelia",
    class_id="magic_user",
    alignment=Alignment.NEUTRAL,
    ruleset=rules,
    stream=streams.get(CHARACTER_CREATION_STREAM),
    starting_spell_ids=["magic_missile"],
)
zelia = created.character
prepared = memorize_spells(zelia, definition, catalog, [MemorizedSpell(spell_id="magic_missile")])
assert prepared.accepted

# One goblin target; the registry maps entity ids to the live objects.
template = load_monsters().get("goblin")
goblin = spawn_monster(template, id="monster-0001", stream=streams.get(MONSTER_SPAWN_STREAM))
outcome = cast_spell(
    zelia,
    catalog.get("magic_missile"),
    "missiles",
    profile=caster_profile(definition),
    targets=[goblin],
    ledger=EffectsLedger(),
    clock=GameClock(),
    allocator=IdAllocator(),
    registry={"monster-0001": goblin},
    ruleset=rules,
    stream=streams.get(MAGIC_STREAM),
    effects_stream=streams.get(EFFECTS_STREAM),
)
assert outcome.spell_id == "magic_missile" and not outcome.no_effect
assert outcome.affected_ids == ("monster-0001",)
assert zelia.memorized_spells == ()  # the cast consumed the memorized copy
assert goblin.max_hp - goblin.current_hp == 3  # 1d6+1 missile damage, stable under this seed

caster_profile

caster_profile(definition: ClassDefinition) -> CasterProfile | None

Return a class definition's casting profile, or None for non-casters.

Parameters:

Name Type Description Default
definition ClassDefinition required

Returns:

Type Description
CasterProfile | None

The profile, from the divine_magic/arcane_magic tag's spell_list param.

disrupt_casting

disrupt_casting(caster: Any, spell_id: str, *, reversed: bool = False) -> list[Event]

Disrupt a declared casting: the copy is lost "as if it had been cast".

The trigger — lost initiative, then successfully attacked or failed a save before acting — is detected by the battle machine; the kernel resolves a disruption when told it happened. The exactly-matching copy is removed first; failing that, any copy of the spell (a divine caster's reversed declaration consumes a normal copy, mirroring the cast-time rule).

Parameters:

Name Type Description Default
caster Any

The disrupted caster: a Character; its memorized_spells tuple shrinks by one copy.

required
spell_id str

The declared spell's id — see the spell id index.

required
reversed bool

Whether the declared cast was the reversed form.

False

Returns:

Type Description
list[Event]

The disruption event.

Raises:

Type Description
ValueError

If no copy of the spell is memorized (programmer misuse).

forget_excess_memorized

forget_excess_memorized(caster: Any, definition: ClassDefinition, catalog: SpellCatalog) -> list[Event]

Forget memorized copies in excess of the caster's (shrunk) slots.

The drain interplay: after a level drop, for each spell level where the memorized count exceeds the new slot count, excess copies are forgotten newest-first (highest tuple index). RAW is silent on which copies go; osrlib adopts newest-first because it keeps the rule deterministic without new state.

Parameters:

Name Type Description Default
caster Any

The drained caster: a Character; its memorized_spells tuple shrinks.

required
definition ClassDefinition

The caster's ClassDefinition.

required
catalog SpellCatalog

The loaded spell catalog, from load_spells.

required

Returns:

Type Description
list[Event]

One forgotten event per dropped copy, newest first.

memorize_spells

memorize_spells(
    caster: Any, definition: ClassDefinition, catalog: SpellCatalog, selections: Sequence[MemorizedSpell]
) -> MemorizationResult

Prepare a caster's daily spells — a full replacement of the memorized list.

Models the daily preparation: the new list wholly replaces the old (partial top-ups are not a B/X operation). Divine casters choose freely from their class list and never fix the reversed form (they reverse at cast time, "by speaking the words and performing the gestures backwards"); arcane casters choose from their spell book and fix normal or reversed per copy at memorization. Duplicate selections are legal per RAW ("may opt to memorize the same spell twice"). The once-a-day/after-sleep/one-hour gates are exploration procedure owned by the crawl layer — standalone callers may call this freely, by design.

Parameters:

Name Type Description Default
caster Any

The preparing caster: a Character with a casting class; its memorized_spells tuple is replaced.

required
definition ClassDefinition

The caster's ClassDefinition.

required
catalog SpellCatalog

The loaded spell catalog, from load_spells.

required
selections Sequence[MemorizedSpell]

The prepared MemorizedSpell copies, in memorization order (order is load-bearing: casting consumes the first matching copy and drain forgets newest-first).

required

Returns:

Type Description
MemorizationResult

The rejections (nothing mutated) or the memorized event.

minimum_caster_level

minimum_caster_level(spell: SpellTemplate) -> int

Return the minimum class level able to cast a spell, per the compiled progressions.

RAW is silent on a scroll's caster level; osrlib adopts the least-power reading (a documented adaptation): the lowest level at which any class on the spell's list has a slot of the spell's level.

Parameters:

Name Type Description Default
spell SpellTemplate required

Returns:

Type Description
int

The minimum caster level.

Raises:

Type Description
ValueError

If no class on the spell's list ever gains a slot of that level.

pop_mirror_image

pop_mirror_image(ledger: EffectsLedger, target_ref: str, *, registry: dict[str, Any], clock: GameClock) -> list[Event]

Destroy one mirror image — called by the game or battle machine per incoming attack.

"Attacks on the caster destroy one of the mirror images (even if the attack misses)." When the last image pops, the effect is released.

Parameters:

Name Type Description Default
ledger EffectsLedger required
target_ref str

The mirrored caster's entity id.

required
registry dict[str, Any]

Live combatants by entity id — Character and MonsterInstance objects the release may mutate.

required
clock GameClock

The GameClock, stamped on the bookkeeping event.

required

Returns:

Type Description
list[Event]

The pop (and final release) events; empty when no mirror-image effect is

list[Event]

active.

turn_undead

turn_undead(
    cleric: Any,
    definition: ClassDefinition,
    candidates: Sequence[Any],
    *,
    ledger: EffectsLedger,
    clock: GameClock,
    allocator: Any,
    registry: dict[str, Any],
    stream: RngStream
) -> TurnUndeadResult

Turn undead — the full procedure, one call.

One 2d6 turn roll (the magic stream) is compared per candidate type against that type's turning-table cell — types are unaffected, number types succeed when the roll meets the threshold, T/D succeed automatically. If any type succeeded, one 2d6 HD-pool roll follows; eligible monsters are affected lowest-HD-first (stable input order on ties), each costing its HD count (minimum 1, fixed bonuses dropped — the sleep convention); the pool stops at the first unaffordable monster (RAW: excess Hit Dice "are wasted", not reallocated); at least one undead is always affected on a successful turn even when the pool rolls short (RAW minimum effect), resolved as the cheapest eligible monster. Affected monsters whose column says D die permanently ("instantly and permanently annihilated"); the rest gain the turned condition via an indefinite, non-dispellable effect the encounter releases (flee behavior is the battle machine's).

Only monsters bearing the undead category are candidates; non-undead in the list resolve as unaffected rather than rejecting, so a turning attempt never doubles as a free undead detector.

Parameters:

Name Type Description Default
cleric Any

The turning character: a Character.

required
definition ClassDefinition

The character's ClassDefinition (the turn_undead tag).

required
candidates Sequence[Any]

The encounter's monsters, in stable order: MonsterInstance objects.

required
ledger EffectsLedger

The EffectsLedger the turned condition attaches through.

required
clock GameClock

The GameClock.

required
allocator Any

The id allocator for the attached effect: an IdAllocator.

required
registry dict[str, Any]

Live combatants by entity id — Character and MonsterInstance objects.

required
stream RngStream

The magic stream — the player rolls turning dice in B/X, so both rolls are player-visible on the event.

required

Returns:

Type Description
TurnUndeadResult

The turning outcome with its events.

Raises:

Type Description
ValueError

If the character cannot turn — validate with validate_turn_undead first.

validate_cast

validate_cast(
    caster: Any,
    spell: SpellTemplate,
    mode: str,
    *,
    profile: CasterProfile | None,
    reversed: bool = False,
    targets: Sequence[object] = (),
    context: CastContext | None = None,
    ledger: EffectsLedger | None = None
) -> list[Rejection]

Validate a cast — the pure pre-phase: no RNG draws, no mutation.

Checks caster capacity (dead, petrified, paralysed, asleep, silenced, feebleminded, or weakened cannot cast; bound/gagged arrive as context flags; an active anti-magic shell blocks the caster's own casting when a ledger is supplied), a matching memorized copy, form and mode legality, target counts (single takes one; magic missile takes exactly one target per missile), and range — only when the context supplies a distance, mirroring the combat convention. Category and immunity gates are resolution outcomes, never validator rejections, by design: casting charm person at a disguised doppelgänger must not be a zero-cost detector. A cleric's holy symbol is not checked — the OSE SRD states carrying one as a class edict, not a mechanical gate on any procedure.

Parameters:

Name Type Description Default
caster Any

The casting caster: a Character.

required
spell SpellTemplate

The SpellTemplate to cast.

required
mode str

The mode key on the chosen form (see SpellTemplate.mode).

required
profile CasterProfile | None

The caster's CasterProfile, from caster_profile on the definition the caller holds — divine casters match any memorized copy (the reversed form is chosen at cast). None skips the memorized-copy check entirely, for scroll reads: the scroll is the copy.

required
reversed bool

True to cast the reversed form.

False
targets Sequence[object]

The explicit target list, per the combatant convention (see osrlib.core.combat): Character or MonsterInstance objects, or location strings for effects games attach to places.

()
context CastContext | None

The caller-asserted CastContext.

None
ledger EffectsLedger | None

The EffectsLedger, consulted for the caster's own blocking effects.

None

Returns:

Type Description
list[Rejection]

Structured rejections; empty when the cast may resolve.

validate_turn_undead

validate_turn_undead(cleric: Any, definition: ClassDefinition) -> list[Rejection]

Validate a turning attempt — the pure pre-phase.

Turning is gated by the turn_undead class-ability tag; an incapacitated cleric (dead, petrified, paralysed, asleep) cannot present the symbol, and a weakened one cannot turn — the raise-dead weakness bans class abilities ("cannot attack, cast spells, or use other class abilities"). The holy symbol itself is not a precondition: the OSE SRD states carrying one as a class edict, not a mechanical gate; games wanting the stricter reading check inventory themselves.

Parameters:

Name Type Description Default
cleric Any

The turning character: a Character.

required
definition ClassDefinition

The character's ClassDefinition.

required

Returns:

Type Description
list[Rejection]

Structured rejections; empty when the attempt may be rolled.