osrlib.core.events
Read what the rules did: the event base class and the events the core rules report.
osrlib's rules functions report what happened as events, and never print anything. An event is a frozen record of one
thing the rules did, like an attack roll or a creature's death. This module defines the base class every event shares,
and the events of the core rules: combat, effects, and magic. You get events from the rules functions, and you rarely
construct one yourself. resolve_attack, for example, returns its events in the
events field of its result, and EffectsLedger.advance returns a list.
You decide what to do with them. osrlib.crawl.events defines the events of a crawl on the
same base class.
The members, in the order you use them:
Eventis the base class. Annotate with it, and readcodeandvisibilityon any event.Visibilityhas the values for who may see an event: the players, or only the referee.parse_eventturns a stored event back into its class.parse_any_eventdoes the same for a log that also has crawl events.KernelEventis the union of the core rules' event classes, for annotations and pydantic validation, andKERNEL_EVENT_CLASSESis the same classes as a tuple.- The remaining classes are the events themselves, one for each kind of thing the rules do.
Every event has structured fields and a message code, never a sentence. A code is a lowercase string of two or
more parts separated by dots, like combat.attack.hit, and it identifies the outcome. Turn one into English with
format_message, write your own formatter, or pass the fields to a narrator.
Every event has a visibility, because in B/X the referee makes some rolls out of the players' sight. Monster hit
points and morale rolls are for the referee only. Filter on Visibility before you
show a log to a player, or let GameSession.view do it for you.
The events can gain fields and types without breaking your program. Within a schema_version, the version stamp
of the saved format, events only gain fields and types. osrlib ignores an unknown field rather than rejecting it,
and parse_event returns None for an event_type your version of osrlib doesn't define. Write your program to
skip what it doesn't recognize, and it can read a log from a newer release.
Each class has an event_type, a fixed string that identifies the class in a serialized event. Tell the classes
apart by event_type, not by code: the code identifies the outcome, and one class can have codes for several
outcomes.
Typical usage:
from osrlib.core.events import DamageDealtEvent, MoraleCheckedEvent, Visibility, parse_event
from osrlib.messages import format_message
# Built by hand here; in a program, these come back from a rules call.
log = [
DamageDealtEvent(target_id="monster-0001", attacker_id="hild", amount=5, rolls=(5,)),
MoraleCheckedEvent(code="combat.morale.held", subject="goblins", score=8, roll=6),
]
# The players see their own half of it.
player_lines = [format_message(event) for event in log if event.visibility is Visibility.PLAYER]
assert player_lines == ["monster-0001 takes 5 damage from hild."]
# And the whole log round-trips through JSON and back.
restored = [parse_event(event.model_dump(mode="json")) for event in log]
assert restored == log
KERNEL_EVENT_CLASSES
module-attribute
KERNEL_EVENT_CLASSES: tuple[type[Event], ...] = (
InitiativeRolledEvent,
AttackRolledEvent,
DamageDealtEvent,
DamageAbsorbedEvent,
SavingThrowRolledEvent,
MoraleCheckedEvent,
ReactionRolledEvent,
ConditionGainedEvent,
ConditionRemovedEvent,
EffectAttachedEvent,
EffectTickedEvent,
EffectExpiredEvent,
EffectReleasedEvent,
HealingAppliedEvent,
DeathEvent,
EquipmentDestroyedEvent,
LevelDrainedEvent,
MonsterRevivedEvent,
HitPointsReportedEvent,
TargetsSelectedEvent,
SpellsMemorizedEvent,
SpellCastEvent,
SpellDisruptedEvent,
SpellForgottenEvent,
SpellBookUpdatedEvent,
UndeadTurnedEvent,
MagicDispelledEvent,
)
Every event class of the core rules, as a tuple, in the order this module defines them.
osrlib calls the core rules the kernel, so these are its kernel events. Iterate the tuple when you need every
class rather than one event: to build a table of handlers, to generate a JSON Schema for each class, or to check
that your program handles every class. The same classes make up
KernelEvent, which is what you annotate and validate with.
It has only the core rules' events. For those and the crawl's events, use
ALL_EVENT_CLASSES.
KernelEvent
module-attribute
KernelEvent = Annotated[
InitiativeRolledEvent
| AttackRolledEvent
| DamageDealtEvent
| DamageAbsorbedEvent
| SavingThrowRolledEvent
| MoraleCheckedEvent
| ReactionRolledEvent
| ConditionGainedEvent
| ConditionRemovedEvent
| EffectAttachedEvent
| EffectTickedEvent
| EffectExpiredEvent
| EffectReleasedEvent
| HealingAppliedEvent
| DeathEvent
| EquipmentDestroyedEvent
| LevelDrainedEvent
| MonsterRevivedEvent
| HitPointsReportedEvent
| TargetsSelectedEvent
| SpellsMemorizedEvent
| SpellCastEvent
| SpellDisruptedEvent
| SpellForgottenEvent
| SpellBookUpdatedEvent
| UndeadTurnedEvent
| MagicDispelledEvent,
Field(discriminator="event_type"),
]
The union of every event class of the core rules, told apart by event_type.
Annotate with it where a value is one event of the core rules and you want its exact class back. Pass it to a
pydantic TypeAdapter to validate serialized events. Pydantic reads event_type and goes straight to the right
class instead of trying each in turn, and the JSON Schema it generates lists which class each event_type means, for
programs that read the schema.
To read back a stored log, call parse_event instead. It validates the same
way, but returns None for an event type your version of osrlib doesn't define, rather than raising. Annotate
with Event instead where any event will do.
AttackRolledEvent
Bases: Event
One attack roll and everything that went into it.
attack_roll reports one of these for every attack, and
resolve_attack reports it before the damage that follows. Read code
for the outcome, and the number fields to show the arithmetic.
An automatic hit has no roll. It happens against a paralysed or sleeping defender in melee, or a monster with
no armour class, like green slime. Then roll, total, required, and defender_ac are None.
allowed_codes
class-attribute
allowed_codes: frozenset[str] = frozenset({'combat.attack.hit', 'combat.attack.missed', 'combat.attack.auto_hit'})
The three outcomes: combat.attack.hit, combat.attack.missed, and combat.attack.auto_hit for an
automatic hit.
event_type
class-attribute
instance-attribute
event_type: Literal['attack_rolled'] = 'attack_rolled'
The fixed string that identifies this event class in a serialized event.
visibility
class-attribute
instance-attribute
visibility: Visibility = Visibility.PLAYER
Player visibility: attack rolls happen in the open.
attack_name
instance-attribute
attack_name: str
The name of what the attacker used: a weapon, a gear item, a magic item, or a monster's attack. It's
"unarmed" for an unarmed attack, and "improvised" for an attack with a gear item's combat stats alone.
roll
class-attribute
instance-attribute
roll: int | None = None
The d20 as it landed, before modifiers. None on an automatic hit.
modifier
class-attribute
instance-attribute
modifier: int = 0
The sum of everything added to the roll, like STR, range, magic, spells, and your situational modifier. It can be negative.
total
class-attribute
instance-attribute
total: int | None = None
The roll plus the modifier, which is the number compared with required. None on an automatic hit.
required
class-attribute
instance-attribute
required: int | None = None
The total the attacker needed to hit, from its THAC0 and the defender's armour class. None on an
automatic hit.
defender_ac
class-attribute
instance-attribute
defender_ac: int | None = None
The defender's armour class as the roll used it, after its shield, spells, and the situation. None on
an automatic hit.
natural
class-attribute
instance-attribute
natural: int | None = None
1 or 20 when the d20 alone decided the outcome, and None otherwise.
A natural 20 always hits and a natural 1 always misses. This field is set only when that rule changed the
outcome: a 20 whose total was below required, or a 1 whose total reached it.
ConditionGainedEvent
Bases: Event
A creature has fallen asleep, been turned to stone, or otherwise gained a condition.
A condition is a named state, from Condition, that changes what a creature
can do. grant_condition reports this, whether you called it yourself
or EffectsLedger.attach called it while attaching an effect that
gives a condition. It reports no event for a creature immune to the condition, or for one that already has it
from the same effect.
allowed_codes
class-attribute
The only code this event uses.
event_type
class-attribute
instance-attribute
event_type: Literal['condition_gained'] = 'condition_gained'
The fixed string that identifies this event class in a serialized event.
code
class-attribute
instance-attribute
code: str = 'effects.condition.gained'
Fixed at effects.condition.gained.
visibility
class-attribute
instance-attribute
visibility: Visibility = Visibility.PLAYER
Player visibility: a creature going rigid or falling asleep is plain to see.
target_id
instance-attribute
target_id: str
The entity id of the creature that gained the condition.
condition
instance-attribute
condition: str
The condition, as a Condition value like asleep or petrified.
effect_id
class-attribute
instance-attribute
effect_id: str | None = None
The id of the effect that gave the condition and takes it away when it ends, or None for a condition
that doesn't come from an effect. In the core rules, that's dead.
ConditionRemovedEvent
Bases: Event
A creature has lost a condition: it's awake again, no longer stone, no longer entangled.
remove_condition reports this, whether you called it yourself or an
EffectsLedger called it when the effect that gave the condition
expired or was released.
allowed_codes
class-attribute
The only code this event uses.
event_type
class-attribute
instance-attribute
event_type: Literal['condition_removed'] = 'condition_removed'
The fixed string that identifies this event class in a serialized event.
code
class-attribute
instance-attribute
code: str = 'effects.condition.removed'
Fixed at effects.condition.removed.
visibility
class-attribute
instance-attribute
visibility: Visibility = Visibility.PLAYER
Player visibility: everyone can see a creature come out of a condition.
effect_id
class-attribute
instance-attribute
effect_id: str | None = None
The id of the effect that gave the condition, or None for a condition that didn't come from an effect.
DamageAbsorbedEvent
Bases: Event
A hit that did nothing, because the target can't be harmed by what hit it.
This comes instead of a DamageDealtEvent when the target is immune to
the damage, like a creature that only silver or magic can harm hit by a steel sword, or a creature immune to
the breath's element. No damage was rolled. Treat it as a hit that did nothing, not as a miss.
allowed_codes
class-attribute
The only code this event uses.
event_type
class-attribute
instance-attribute
event_type: Literal['damage_absorbed'] = 'damage_absorbed'
The fixed string that identifies this event class in a serialized event.
code
class-attribute
instance-attribute
code: str = 'combat.damage.absorbed'
Fixed at combat.damage.absorbed.
visibility
class-attribute
instance-attribute
visibility: Visibility = Visibility.PLAYER
Player visibility: the players see the hit land and do nothing.
attacker_id
class-attribute
instance-attribute
attacker_id: str | None = None
The entity id of the attacker, or None when there was no attacker.
DamageDealtEvent
Bases: Event
Damage dealt to a creature.
deal_damage reports one of these each time it deals damage, whether the
damage came from a weapon, a spell, or an effect like burning oil.
It reports how much damage was dealt and never how many hit points the target has left, because in B/X only
the referee sees monster hit points. The hit points left are in the
HitPointsReportedEvent that follows it, which only the referee
sees.
allowed_codes
class-attribute
The only code this event uses.
event_type
class-attribute
instance-attribute
event_type: Literal['damage_dealt'] = 'damage_dealt'
The fixed string that identifies this event class in a serialized event.
code
class-attribute
instance-attribute
code: str = 'combat.damage.dealt'
Fixed at combat.damage.dealt.
visibility
class-attribute
instance-attribute
visibility: Visibility = Visibility.PLAYER
Player visibility: everyone sees how hard a creature was hit.
attacker_id
class-attribute
instance-attribute
attacker_id: str | None = None
The entity id of the attacker, or None when there was no attacker, as for a fall or a trap.
rolls
class-attribute
instance-attribute
Each damage die as rolled, before modifiers. Empty for damage that wasn't rolled, like a dragon's breath.
keys
class-attribute
instance-attribute
The keys of the damage that the target's defenses check: keys like silver, magic, or holy, then the
damage's element, like fire, when it had one.
non_regenerable
class-attribute
instance-attribute
non_regenerable: bool = False
True when the target regenerates and this damage's element stops its regeneration, as fire and acid do for a troll. Such damage can't be regenerated, and once enough of it adds up, the troll stays dead.
DeathEvent
Bases: Event
A creature has been killed.
kill reports this, whether the death came from damage, a failed save, or an
effect running out. It comes after the ConditionGainedEvent for
dead, and before the HitPointsReportedEvent.
deal_damage reports a second one, with the code combat.death.permanent,
when the damage a dead monster can't regenerate adds up to its maximum hit points. That one
comes after the HitPointsReportedEvent, with no condition event, because the monster is already dead.
Read the code to know whether the death can be undone. An ordinary death can be, by magic or by regeneration. A permanent one can't, which is what enough fire and acid do to a troll.
allowed_codes
class-attribute
The two outcomes: combat.death.died for an ordinary death, and combat.death.permanent for one that
can't be undone.
event_type
class-attribute
instance-attribute
event_type: Literal['death'] = 'death'
The fixed string that identifies this event class in a serialized event.
visibility
class-attribute
instance-attribute
visibility: Visibility = Visibility.PLAYER
Player visibility: everyone sees a creature fall.
EffectAttachedEvent
Bases: Event
An effect has started on a creature, an item, or a location.
An effect is a lasting change the rules make, like a spell's sleep or a troll's regeneration, which an
EffectsLedger tracks until it ends.
EffectsLedger.attach reports this first, before any condition the
effect gives. Keep the effect_id if you plan to end the effect early with
EffectsLedger.release.
allowed_codes
class-attribute
The only code this event uses.
event_type
class-attribute
instance-attribute
event_type: Literal['effect_attached'] = 'effect_attached'
The fixed string that identifies this event class in a serialized event.
code
class-attribute
instance-attribute
code: str = 'effects.effect.attached'
Fixed at effects.effect.attached.
visibility
class-attribute
instance-attribute
visibility: Visibility = Visibility.REFEREE
Referee visibility: this is the ledger's record. The players notice the condition the effect gives, which a separate event reports.
kind
instance-attribute
kind: str
The effect's kind, from its definition, like "sleep", "regeneration", or "light".
target_ref
instance-attribute
target_ref: str
What the effect is attached to: an entity id for a creature, or a location string for an effect in a place.
expires_round
class-attribute
instance-attribute
expires_round: int | None = None
The round the effect is due to end, counted from the start of the game clock, like
GameClock.rounds. None when the effect has no duration or is
permanent.
EffectExpiredEvent
Bases: Event
An effect ran out of time and ended.
EffectsLedger.advance reports this when the clock reaches the
round an effect ends on. This event comes first. Then the effect's condition and modifiers come off. Last, an
effect that does something when it ends does it, like the second hit of thrown oil. When you end an effect early,
the ledger reports an EffectReleasedEvent instead.
allowed_codes
class-attribute
The only code this event uses.
event_type
class-attribute
instance-attribute
event_type: Literal['effect_expired'] = 'effect_expired'
The fixed string that identifies this event class in a serialized event.
code
class-attribute
instance-attribute
code: str = 'effects.effect.expired'
Fixed at effects.effect.expired.
visibility
class-attribute
instance-attribute
visibility: Visibility = Visibility.REFEREE
Referee visibility: this is the ledger's record. When a light source burns out, a session also reports a separate event the players see.
target_ref
instance-attribute
target_ref: str
The entity id or location string the effect was attached to.
round
instance-attribute
round: int
The round the effect ended on, counted from the start of the game clock.
EffectReleasedEvent
Bases: Event
An effect was ended early, before its time was up.
EffectsLedger.release reports this, as when a dispel magic
ends a spell, a charmed creature makes its save, or someone puts out a torch. For an effect that ran out of time,
the ledger reports an EffectExpiredEvent instead.
allowed_codes
class-attribute
The only code this event uses.
event_type
class-attribute
instance-attribute
event_type: Literal['effect_released'] = 'effect_released'
The fixed string that identifies this event class in a serialized event.
code
class-attribute
instance-attribute
code: str = 'effects.effect.released'
Fixed at effects.effect.released.
visibility
class-attribute
instance-attribute
visibility: Visibility = Visibility.REFEREE
Referee visibility: this is the ledger's record.
target_ref
instance-attribute
target_ref: str
The entity id or location string the effect was attached to.
EffectTickedEvent
Bases: Event
An effect did what it does at regular intervals, like a troll's regeneration healing it.
EffectsLedger.advance reports this each time a regeneration
heals or a charmed creature rolls its repeated save, and
pop_mirror_image reports it when an attack destroys one image. What
the effect did is in the events next to this one: healing, a saving throw, or a release.
allowed_codes
class-attribute
The only code this event uses.
event_type
class-attribute
instance-attribute
event_type: Literal['effect_ticked'] = 'effect_ticked'
The fixed string that identifies this event class in a serialized event.
code
class-attribute
instance-attribute
code: str = 'effects.effect.ticked'
Fixed at effects.effect.ticked.
visibility
class-attribute
instance-attribute
visibility: Visibility = Visibility.REFEREE
Referee visibility: this is the ledger's record.
target_ref
instance-attribute
target_ref: str
The entity id or location string the effect is attached to.
round
instance-attribute
round: int
The round the effect acted on, counted from the start of the game clock.
EquipmentDestroyedEvent
Bases: Event
Everything a victim carried was destroyed with it.
destroy_equipment reports this when a death destroys the victim and
what it carries, as a dragon's breath or disintegrate does. It reports no event for a victim that carried
nothing.
When the ruleset's magic_item_death_save flag is on, destroy_equipment rolls a save for each magic item, and
saved_items lists the ones that survive. They stay in the victim's inventory.
allowed_codes
class-attribute
The only code this event uses.
event_type
class-attribute
instance-attribute
event_type: Literal['equipment_destroyed'] = 'equipment_destroyed'
The fixed string that identifies this event class in a serialized event.
code
class-attribute
instance-attribute
code: str = 'combat.equipment.destroyed'
Fixed at combat.equipment.destroyed.
visibility
class-attribute
instance-attribute
visibility: Visibility = Visibility.PLAYER
Player visibility: the players see what was lost.
item_names
instance-attribute
The display names of the items that were destroyed.
Event
Bases: BaseModel
The base class of every osrlib event: a frozen record of one thing the rules did.
Use it as the type you annotate with, as in list[Event]. The two fields declared here are the two you can
read on any event: code, which identifies what happened, and visibility, which is who may see it. The
other fields are on each subclass. Check event_type or use isinstance to find out which subclass you have.
Subclass it to add events of your own, for rules osrlib doesn't cover. Give the subclass structured fields
only, report each outcome as a code rather than a sentence, and give it an event_type that differs from every
osrlib event's. parse_event reads only osrlib's events and returns None for
yours, so read yours back with your class's model_validate. Defining a subclass that turns off frozen, or
sets extra to anything but "ignore", raises TypeError. Either one would break two promises osrlib
makes about every event: the event never changes, and osrlib ignores its unknown fields.
Events never change once made, so you can keep a list of them as a log and read it at any time.
Examples:
from pydantic import ValidationError
from osrlib.core.events import DamageDealtEvent
event = DamageDealtEvent(target_id="monster-0001", amount=5)
assert event.code == "combat.damage.dealt"
# A code the class doesn't declare is refused.
try:
DamageDealtEvent(code="combat.damage.absorbed", target_id="monster-0001", amount=5)
except ValidationError as error:
assert "combat.damage.dealt" in str(error)
allowed_codes
class-attribute
The codes a subclass may use. Building an event with any other code raises a pydantic ValidationError.
It's empty on the base class, which accepts any code in the right format. Read it to find out which codes a class accepts. Set it on your own subclass to get the same check.
code
instance-attribute
code: str
What happened, like combat.attack.hit: two or more lowercase parts separated by dots. The first part is
the area of the rules the event comes from, like combat or magic.
Branch on it, and pass the event to format_message to turn it into English.
Building an event with a code in any other format raises a pydantic ValidationError.
visibility
instance-attribute
visibility: Visibility
Who may see the event. Filter on it before you show a log to a player.
HealingAppliedEvent
Bases: Event
Hit points restored, or healing refused.
apply_healing reports this, and so does
EffectsLedger.advance each round a regeneration effect heals. Read
amount, not the amount you asked for: healing stops at the creature's maximum hit points, and a cursed creature
may have had it halved.
apply_healing can also refuse healing. Then the code is combat.healing.blocked and amount is 0. A diseased
creature gets no magical healing, and a weakened one gets no healing at all.
allowed_codes
class-attribute
The two outcomes: combat.healing.applied, and combat.healing.blocked when a condition prevented
the healing.
event_type
class-attribute
instance-attribute
event_type: Literal['healing_applied'] = 'healing_applied'
The fixed string that identifies this event class in a serialized event.
visibility
class-attribute
instance-attribute
visibility: Visibility = Visibility.PLAYER
Player visibility: everyone sees a wound close. The hit points the creature now has are in the
HitPointsReportedEvent that follows, which only the referee
sees.
amount
instance-attribute
amount: int
The hit points restored, after the maximum and any halving. 0 when the healing was blocked.
source
instance-attribute
source: str
Where the healing came from: magical for a spell or potion, natural for rest, or regeneration for a
regenerating creature.
HitPointsReportedEvent
Bases: Event
A creature's hit points after something changed them.
Every rules function that changes hit points reports one of these right after the change: the functions for damage, healing, death, energy drain, and regeneration. Follow them and you know every creature's hit points without reading the creatures.
This is the only event with a creature's current and maximum hit points. The others report only the change:
a DamageDealtEvent the damage, and a
LevelDrainedEvent the maximum hit points lost. Only the referee
sees this event, because in B/X the players don't see monster hit points. Show the players the events that
report the change.
allowed_codes
class-attribute
The only code this event uses.
event_type
class-attribute
instance-attribute
event_type: Literal['hit_points_reported'] = 'hit_points_reported'
The fixed string that identifies this event class in a serialized event.
code
class-attribute
instance-attribute
code: str = 'combat.state.hit_points'
Fixed at combat.state.hit_points.
visibility
class-attribute
instance-attribute
visibility: Visibility = Visibility.REFEREE
Referee visibility: in B/X, the players don't see monster hit points.
max_hp
instance-attribute
max_hp: int
The creature's maximum hit points, which energy drain can lower.
InitiativeRoll
Bases: BaseModel
One initiative roll on an InitiativeRolledEvent.
Read these from the event's entries to show what each side or combatant rolled. Check the event's
mode to know which: a side under side initiative, a combatant under individual initiative.
key
instance-attribute
key: str
Who rolled: a side's name under side initiative, or a combatant's entity id under individual initiative.
rolls
instance-attribute
Every d6 rolled for this side or combatant, in the order rolled. roll_initiative re-rolls a tie, so more
than one entry means there was a tie, and the later entries are the rolls that broke it. The last entry is the
one that counts.
modifier
class-attribute
instance-attribute
modifier: int = 0
The modifier added to the last roll. It's 0 under side initiative. Under individual initiative, a
character's comes from its DEX and class, through
participant_modifier, and a monster's is the one you chose.
total
instance-attribute
total: int
The last roll plus the modifier. roll_initiative sorts the acting order on this total.
InitiativeRolledEvent
Bases: Event
Who acts first this round, and what everyone rolled to get there.
roll_initiative reports one of these each time it's called, which is
at the start of each combat round. Read order to know who acts next, and entries to show the dice.
allowed_codes
class-attribute
The only code this event uses.
event_type
class-attribute
instance-attribute
event_type: Literal['initiative_rolled'] = 'initiative_rolled'
The fixed string that identifies this event class in a serialized event.
code
class-attribute
instance-attribute
code: str = 'combat.initiative.rolled'
Fixed at combat.initiative.rolled.
visibility
class-attribute
instance-attribute
visibility: Visibility = Visibility.PLAYER
Player visibility: everyone at the table can see the order of play.
mode
instance-attribute
mode: Literal['side', 'individual']
How initiative was rolled: "side" when each side rolled once for all its members, "individual" when
each combatant rolled for itself.
entries
instance-attribute
entries: tuple[InitiativeRoll, ...]
One InitiativeRoll for each side, or for each combatant under
individual initiative. They're in the order roll_initiative received them, not the order they act.
LevelDrainedEvent
Bases: Event
An undead creature's touch has taken levels, or killed its victim.
drain_levels reports this for a character, and
drain_monster_hd for a monster, which loses Hit Dice instead of
levels.
When the victim has nothing left to lose, at level 1 or 1 Hit Die, the drain kills it instead. Then the code
is combat.drain.slain, and a DeathEvent follows. The killing level
counts as lost, so when a spectre, which drains two levels, kills a level-2 fighter, the event shows two levels
lost.
allowed_codes
class-attribute
The two outcomes: combat.drain.drained when levels were lost, and combat.drain.slain when the drain
killed the victim.
event_type
class-attribute
instance-attribute
event_type: Literal['level_drained'] = 'level_drained'
The fixed string that identifies this event class in a serialized event.
visibility
class-attribute
instance-attribute
visibility: Visibility = Visibility.PLAYER
Player visibility: the players know when a level is lost.
new_level
instance-attribute
new_level: int
The victim's level, or Hit Dice, after the drain. 0 when the drain killed it.
xp_after
class-attribute
instance-attribute
xp_after: int | None = None
The character's experience points after the drain. Depending on the draining monster, they're either
halfway between the old and new levels' thresholds, or at the new level's threshold. None for a monster, whose Hit
Dice are worth no experience points, and for a victim the drain killed.
spawn_consequence
class-attribute
instance-attribute
spawn_consequence: str | None = None
What the rules say becomes of a character this undead creature kills, like rising as a wight in 1d4
days under the control of its killer. osrlib kills the victim, and playing out what it becomes is up to you.
None for a monster victim, and when the undead creature's rules say nothing about it.
MagicDispelledEvent
Bases: Event
A dispel magic ended the effects on its targets, except those that survived.
cast_spell reports this when dispel magic resolves. Only effects marked
dispellable can end: every effect a spell attaches is, and what a monster inflicts isn't. An effect cast by a
higher-level caster gets a roll to survive, with a better chance the
wider the gap in levels, so one dispel can end some effects and leave others. Each effect that ended also has
an EffectReleasedEvent, before this event.
allowed_codes
class-attribute
The only code this event uses.
event_type
class-attribute
instance-attribute
event_type: Literal['magic_dispelled'] = 'magic_dispelled'
The fixed string that identifies this event class in a serialized event.
code
class-attribute
instance-attribute
code: str = 'magic.dispel.resolved'
Fixed at magic.dispel.resolved.
visibility
class-attribute
instance-attribute
visibility: Visibility = Visibility.PLAYER
Player visibility: the players see the spell and what it ended.
released_effect_ids
class-attribute
instance-attribute
The ids of the effects the spell ended.
MonsterRevivedEvent
Bases: Event
A monster the party thought dead has got back up.
EffectsLedger.advance reports this for a regeneration effect
when the monster's countdown runs out,
which for a troll is 2d6 rounds after it was killed. The monster comes back with 1 hit point, and loses the
dead condition at the same time. A monster stops getting back up once the damage it can't regenerate, fire
and acid for a troll, adds up to its maximum hit points.
allowed_codes
class-attribute
The only code this event uses.
event_type
class-attribute
instance-attribute
event_type: Literal['monster_revived'] = 'monster_revived'
The fixed string that identifies this event class in a serialized event.
code
class-attribute
instance-attribute
code: str = 'effects.regeneration.revived'
Fixed at effects.regeneration.revived.
visibility
class-attribute
instance-attribute
visibility: Visibility = Visibility.PLAYER
Player visibility: the players see the monster get back up.
MoraleCheckedEvent
Bases: Event
A side's morale check, and whether the side held.
check_morale reports one of these each time a side checks morale. A side
that breaks flees or surrenders, and making it do so is up to you.
At some morale scores, a side never rolls. A side with a score of 2 or less never fights, and a side with 12 or
more never checks. check_morale reports both with combat.morale.exempt and no roll. Read held to tell them
apart, as for any other code: at 2 or less the side has broken, and at 12 or more it holds.
allowed_codes
class-attribute
allowed_codes: frozenset[str] = frozenset({'combat.morale.held', 'combat.morale.broke', 'combat.morale.exempt'})
The three codes: combat.morale.held, combat.morale.broke, and combat.morale.exempt when the side's
score means it doesn't roll. The exempt code covers both exempt scores, so read held to tell them apart.
event_type
class-attribute
instance-attribute
event_type: Literal['morale_checked'] = 'morale_checked'
The fixed string that identifies this event class in a serialized event.
visibility
class-attribute
instance-attribute
visibility: Visibility = Visibility.REFEREE
Referee visibility: the players learn that a side has broken by watching it run.
subject
instance-attribute
subject: str
The key of the side whose morale was checked, as you passed it.
score
instance-attribute
score: int
The side's morale score. A side with a score from 3 to 11 rolls. A side with 2 or less doesn't roll and has broken, and a side with 12 or more doesn't roll and holds.
roll
class-attribute
instance-attribute
roll: int | None = None
The 2d6 total, before the modifier. None when the side didn't roll.
modifier
class-attribute
instance-attribute
modifier: int = 0
The adjustment applied, after it was limited to the range −2 to +2.
held
class-attribute
instance-attribute
held: bool | None = None
Whether the side keeps fighting.
check_morale sets it on every event, whatever the code. After a roll, it's
the outcome of the roll. It's True for a score of 12 or more, and False for a score of 2 or less. Show the
outcome from this field, not from the code and the score. It's None on an event loaded from a save written
before the field existed, because osrlib doesn't fill it in when it loads such a save.
PreparedSpell
Bases: BaseModel
One spell a caster has in memory, as a memorization event lists it.
Read these from the prepared field of a SpellsMemorizedEvent.
A caster who prepares a reversible spell chooses its form when preparing it, not when casting it, so each prepared copy records its form.
ReactionRolledEvent
Bases: Event
How monsters react to meeting the party.
roll_reaction reports one of these each time it's called, which is when
the party meets monsters that aren't already fighting. Use the result to decide how the monsters act.
allowed_codes
class-attribute
The only code this event uses.
event_type
class-attribute
instance-attribute
event_type: Literal['reaction_rolled'] = 'reaction_rolled'
The fixed string that identifies this event class in a serialized event.
code
class-attribute
instance-attribute
code: str = 'encounter.reaction.rolled'
Fixed at encounter.reaction.rolled.
visibility
class-attribute
instance-attribute
visibility: Visibility = Visibility.REFEREE
Referee visibility: the players learn a monster's mood from what it does, not from the dice.
modifier
class-attribute
instance-attribute
modifier: int = 0
The CHA modifier to reactions of the character who spoke to the monsters, or 0 when no one did.
total
instance-attribute
total: int
The roll plus the modifier, which is the number osrlib looks up on the reaction table. osrlib reads a total outside 2 to 12 as the nearest end of the table.
result
instance-attribute
result: str
How the monsters react, as a ReactionResult value: attacks,
hostile, uncertain, indifferent, or friendly.
SavingThrowRolledEvent
Bases: Event
A saving throw and how it went.
saving_throw reports one of these whenever a creature gets a chance to
avoid something. What passing or failing means depends on what forced the save, so read this event with the
events after it.
A monster whose defenses give it an automatic save against a magical effect's element, as a red dragon has
against magical fire, rolls nothing. Then roll and required are None.
allowed_codes
class-attribute
allowed_codes: frozenset[str] = frozenset({'combat.save.passed', 'combat.save.failed', 'combat.save.auto'})
The three outcomes: combat.save.passed, combat.save.failed, and combat.save.auto for a save that
passed without a roll.
event_type
class-attribute
instance-attribute
event_type: Literal['saving_throw_rolled'] = 'saving_throw_rolled'
The fixed string that identifies this event class in a serialized event.
visibility
class-attribute
instance-attribute
visibility: Visibility = Visibility.PLAYER
Player visibility: saving throws are rolled in the open, monsters included.
category
instance-attribute
category: str
Which saving throw it was, as a SaveCategory value: death, wands,
paralysis, breath, or spells.
roll
class-attribute
instance-attribute
roll: int | None = None
The d20 as it landed, before modifiers. None on an automatic save.
modifier
class-attribute
instance-attribute
modifier: int = 0
The sum of everything added to the roll, like a spell, a ring, and your adjustment.
required
class-attribute
instance-attribute
required: int | None = None
The number the roll plus the modifier had to reach. None on an automatic save.
SpellBookUpdatedEvent
Bases: Event
A new spell has gone into an arcane caster's spell book.
add_spell_to_book reports this. A caster may prepare only the
spells in its book. Adding a spell to the book doesn't prepare it. Preparing is a separate step, which
memorize_spells reports with a SpellsMemorizedEvent.
allowed_codes
class-attribute
The only code this event uses.
event_type
class-attribute
instance-attribute
event_type: Literal['spell_book_updated'] = 'spell_book_updated'
The fixed string that identifies this event class in a serialized event.
visibility
class-attribute
instance-attribute
visibility: Visibility = Visibility.PLAYER
Player visibility: the book belongs to the player.
caster_id
instance-attribute
caster_id: str
The entity id of the caster whose book gained the spell.
SpellCastEvent
Bases: Event
A spell was cast, from memory or from a scroll.
cast_spell reports this when a caster casts a memorized spell, which uses
up the memorized copy, and cast_from_scroll reports it when a caster
reads a spell from a scroll. What the spell did comes in the events after this one: saving throws, damage,
conditions, effects, healing, and deaths.
When a cast affects no one, the event has the code magic.cast.no_effect, and the spell is still used up. Refusing
the cast instead would show the player, at no cost, which targets the spell could affect, and the B/X rules don't
reveal that.
allowed_codes
class-attribute
The two outcomes: magic.cast.cast, and magic.cast.no_effect when the spell affected no one.
event_type
class-attribute
instance-attribute
event_type: Literal['spell_cast'] = 'spell_cast'
The fixed string that identifies this event class in a serialized event.
visibility
class-attribute
instance-attribute
visibility: Visibility = Visibility.PLAYER
Player visibility: everyone hears a spell cast aloud.
mode
instance-attribute
mode: str
The key of the spell mode that was cast. For a spell with more than one mode, like one that can harm or heal, it's the mode the caster chose.
reversed
class-attribute
instance-attribute
reversed: bool = False
True when the spell was cast in its reversed form.
target_ids
class-attribute
instance-attribute
The entity ids or locations the caster aimed the spell at. Empty for a spell with no target.
manual
class-attribute
instance-attribute
manual: bool = False
True when osrlib didn't resolve the spell's effect, because the rules leave that mode to the referee. The copy is used up, and you play out the outcome from the spell's text.
SpellDisruptedEvent
Bases: Event
A spell the caster declared came to nothing, and the caster lost it anyway.
It's reported in two cases, and you tell them apart by code:
magic.cast.disrupted:disrupt_castingreports it when a caster who declared a spell is hit, or stopped some other way, before the spell goes off.magic.cast.fizzled: a session reports it during a battle round. In the round's magic phase, the session checks each declared spell again, just before the spell would go off. If a check the spell passed at the start of the round now fails, because something earlier in the round changed, the spell fizzles. Two examples are an ally's silence 15' radius centered on the party's cell, and the party's last torch going out when its bearer dies. Any of the checks can cause a fizzle, andreasonhas the rejection code of the one that failed.
Either way, nothing happened and the memorized copy is gone, as if the spell had been cast. Tell the player the spell failed and the copy is used up. A spell read from a scroll that fizzles uses up the scroll's spell the same way, and the caster loses no memorized copy.
allowed_codes
class-attribute
The two codes: magic.cast.disrupted and magic.cast.fizzled.
event_type
class-attribute
instance-attribute
event_type: Literal['spell_disrupted'] = 'spell_disrupted'
The fixed string that identifies this event class in a serialized event.
code
class-attribute
instance-attribute
code: str = 'magic.cast.disrupted'
Which of the two failures this is, defaulting to magic.cast.disrupted.
visibility
class-attribute
instance-attribute
visibility: Visibility = Visibility.PLAYER
Player visibility: the caster's spell visibly fails.
reversed
class-attribute
instance-attribute
reversed: bool = False
True when the lost copy was the spell's reversed form.
reason
class-attribute
instance-attribute
reason: str | None = None
Why a magic.cast.fizzled spell failed: the code of the first check that failed when the session checked
the spell again during the battle round.
Any check a spell passes at the start of the round can fail, so read the code and don't assume a cause. An
ally's silence on the party's cell gives magic.cast.silenced_area, and a scroll read in the dark gives
exploration.action.requires_light. Match on it as you match on the code of a
Rejection. The rejection code reference explains
each code. None on a magic.cast.disrupted event, whose cause is the hit, and on an event from a log
written before the field existed.
SpellForgottenEvent
Bases: Event
A caster forgot a memorized spell, because the caster no longer has room for it.
forget_excess_memorized reports one of these for each copy the
caster forgets. That happens when energy drain takes a caster's levels, and the caster has fewer spell slots
than spells in memory. A caster who forgot two copies gets two events.
allowed_codes
class-attribute
The only code this event uses.
event_type
class-attribute
instance-attribute
event_type: Literal['spell_forgotten'] = 'spell_forgotten'
The fixed string that identifies this event class in a serialized event.
code
class-attribute
instance-attribute
code: str = 'magic.memory.forgotten'
Fixed at magic.memory.forgotten.
visibility
class-attribute
instance-attribute
visibility: Visibility = Visibility.PLAYER
Player visibility: the caster's memory is the player's to track.
reversed
class-attribute
instance-attribute
reversed: bool = False
True when the forgotten copy was the spell's reversed form.
SpellsMemorizedEvent
Bases: Event
A caster has finished preparing spells for the day.
memorize_spells reports one of these each time it prepares a caster's
spells. Preparing replaces everything the caster had in memory, so prepared lists all of the caster's
memory now, not only what was added.
allowed_codes
class-attribute
The only code this event uses.
event_type
class-attribute
instance-attribute
event_type: Literal['spells_memorized'] = 'spells_memorized'
The fixed string that identifies this event class in a serialized event.
code
class-attribute
instance-attribute
code: str = 'magic.memorize.prepared'
Fixed at magic.memorize.prepared.
visibility
class-attribute
instance-attribute
visibility: Visibility = Visibility.PLAYER
Player visibility: the player may see what their caster has memorized.
prepared
instance-attribute
prepared: tuple[PreparedSpell, ...]
Every copy now in the caster's memory, as PreparedSpell entries. A
spell prepared twice appears twice.
TargetsSelectedEvent
Bases: Event
Which of the candidates a spell reached.
select_targets reports this before the spell's other events, so a log
shows which creatures a spell like sleep or hold person reached, not only the creatures it was aimed
at.
allowed_codes
class-attribute
The only code this event uses.
event_type
class-attribute
instance-attribute
event_type: Literal['targets_selected'] = 'targets_selected'
The fixed string that identifies this event class in a serialized event.
code
class-attribute
instance-attribute
code: str = 'combat.targeting.selected'
Fixed at combat.targeting.selected.
visibility
class-attribute
instance-attribute
visibility: Visibility = Visibility.REFEREE
Referee visibility: this is a record for the referee. What happens to the targets comes as separate events.
mode
instance-attribute
mode: str
How the targets were chosen, as a TargetingMode value: self,
single, up_to_n, hd_budget, area, or gaze.
TurningTypeOutcome
Bases: BaseModel
How one kind of monster fared against a cleric's attempt to turn undead.
turn_undead reads the turning table once for each kind of monster present, not once for each monster, and
each of these is one reading. Read them from the types field of an
UndeadTurnedEvent.
column
class-attribute
instance-attribute
column: str | None = None
The turning table column this kind is read in, which depends on its Hit Dice. None when the kind is too
strong to have a column, or isn't undead.
outcome
instance-attribute
outcome: str
The result for this kind, as one of four strings.
turn means the roll reached the threshold, or the table's entry is an automatic turn. fail means the roll
fell short, the table's entry gives the cleric no chance, or the kind has no column. destroy means the
table's entry at the cleric's level is an automatic destroy. unaffected means the monster isn't undead.
threshold
class-attribute
instance-attribute
threshold: int | None = None
The 2d6 total the cleric needed for this kind. None when the table has an automatic result instead of
a number.
UndeadTurnedEvent
Bases: Event
A cleric tried to turn undead, and this is what happened.
turn_undead reports one of these for each attempt. Turning takes two
rolls. turn_undead reads the first on the turning table once for each kind of undead present. When any kind is
turned or destroyed, a second roll sets how many Hit Dice of those kinds are affected. What then happens to each
monster comes as separate events: a condition gained, or a death.
allowed_codes
class-attribute
allowed_codes: frozenset[str] = frozenset({'magic.turning.turned', 'magic.turning.destroyed', 'magic.turning.failed'})
The three outcomes: magic.turning.failed when no kind was turned or destroyed, magic.turning.turned
when monsters were turned, and magic.turning.destroyed when at least one was destroyed.
event_type
class-attribute
instance-attribute
event_type: Literal['undead_turned'] = 'undead_turned'
The fixed string that identifies this event class in a serialized event.
visibility
class-attribute
instance-attribute
visibility: Visibility = Visibility.PLAYER
Player visibility: the player rolls the turning dice.
hd_pool
class-attribute
instance-attribute
hd_pool: int | None = None
The second 2d6: how many Hit Dice of undead the turning affects. None when no kind was turned or
destroyed, so no second roll was made.
turn_undead takes the weakest undead first. When the next one has more Hit Dice than are left, the rest go
unused, and it takes no other monster in its place.
types
class-attribute
instance-attribute
types: tuple[TurningTypeOutcome, ...] = ()
One TurningTypeOutcome for each kind of monster present, in the
order you listed the monsters.
Visibility
Bases: StrEnum
Who may see an event: the players, or only the referee.
Every event has one of these values, because in B/X the referee makes some rolls behind a screen. Filter
a log on it before you show anything to a player, or call
GameSession.view, which builds the players' view and the referee's
view for you. A narrator that plays the referee reads every event.
A serialized event contains the lowercase value. The values stay fixed within a schema_version, the version
stamp of the saved format.
Examples:
from osrlib.core.events import DamageDealtEvent, HitPointsReportedEvent, Visibility
log = [
DamageDealtEvent(target_id="monster-0001", amount=5),
HitPointsReportedEvent(target_id="monster-0001", current_hp=2, max_hp=7),
]
shown = [event.code for event in log if event.visibility is Visibility.PLAYER]
assert shown == ["combat.damage.dealt"] # the goblin's remaining hit points stay hidden
PLAYER
class-attribute
instance-attribute
Anyone may see it. These events are what the players see happen in the game: a hit, a spell, a door opening.
REFEREE
class-attribute
instance-attribute
Only the referee may see it. These events have the numbers that only the referee sees in B/X, like monster hit points and morale, reaction, surprise, and detection rolls. An effects ledger's records of its effects are referee events too.
parse_event
Turn one stored event back into its class, or return None if your version of osrlib doesn't define its type.
Use this to read a saved log, or events sent from another program. It returns None for an event it doesn't
recognize, so an older release can read a log a newer one wrote, except for the events the older one has no
class for. Leave out the None results and go on.
It reads only the core rules' events. To read a log that also has crawl events, call
parse_any_event, which reads both. Neither reads an event class you
defined yourself.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data
|
Mapping[str, object]
|
One event as a mapping, in the form |
required |
Returns:
| Type | Description |
|---|---|
Event | None
|
The event as an instance of its class, or |
Raises:
| Type | Description |
|---|---|
ContentValidationError
|
If osrlib defines the event type but the rest of the mapping doesn't
fit the class, like a required field that's missing. A broken event of a known type is an error in the
log, so |
Examples:
from osrlib.core.events import DamageDealtEvent, parse_event
event = DamageDealtEvent(target_id="monster-0001", attacker_id="hild", amount=5, rolls=(5,))
assert parse_event(event.model_dump(mode="json")) == event
# An event type from a release this one predates is skipped, not an error.
assert parse_event({"event_type": "something_newer", "code": "x.y", "visibility": "player"}) is None