Skip to content

osrlib.crawl.events

The crawl event catalog: typed records of everything a session does.

Every command you run through GameSession.execute comes back with a CommandResult whose events tuple contains instances of the classes here and of the kernel classes in osrlib.core.events. Read them in order, drop the ones your reader may not see, and turn each one into a line with format_message or with a renderer of your own keyed on the event's code. The same objects accumulate on GameSession.event_log, and save_game writes them into a save.

Every event has a code, an event_type, and a visibility. The code is a message code, dot-separated snake_case namespaced by subsystem (exploration.door.opened), and it's what a renderer keys on. The event type is the wire discriminator that names the class, so a serialized event rebuilds into the right one. The visibility says who may see the event: player for what the table learns, referee for the rolls and bookkeeping B/X keeps behind the screen, like a detection die or a wandering check. A class that can report more than one outcome declares its whole code set in allowed_codes, and an instance uses one of them.

An event never contains English prose written by the engine. It contains facts and a code, so a front end can localize, and a narrator can write its own line from the same facts. The exception is a narrative field: that is text the adventure's author wrote, passed through as content.

CRAWL_EVENT_CLASSES is the registry of the classes in this module, and ALL_EVENT_CLASSES adds the kernel ones in front of it. AnyEvent is the union of all of them for typing and JSON Schema, and parse_any_event turns a serialized record back into an event.

Typical usage:

from osrlib.core.alignment import Alignment
from osrlib.core.character import CHARACTER_CREATION_STREAM, create_character
from osrlib.core.events import Visibility
from osrlib.core.rng import RngStreams
from osrlib.core.ruleset import Ruleset
from osrlib.crawl.adventure import Adventure, TownSpec
from osrlib.crawl.commands import EnterDungeon, MoveParty
from osrlib.crawl.dungeon import Direction, DungeonSpec, Edge, EdgeKind, LevelSpec
from osrlib.crawl.party import Party
from osrlib.crawl.session import GameSession
from osrlib.messages import format_message

rules = Ruleset()
stream = RngStreams(master_seed=7).get(CHARACTER_CREATION_STREAM)
hero = create_character(
    name="Hild",
    class_id="fighter",
    alignment=Alignment.LAWFUL,
    ruleset=rules,
    stream=stream,
).character
corridor = LevelSpec(number=1, width=2, height=1, entrance=(0, 0), edges={"1,0:west": Edge(kind=EdgeKind.OPEN)})
crypt = DungeonSpec(id="crypt", name="The Old Crypt", levels=(corridor,))
adventure = Adventure(name="A First Delve", town=TownSpec(name="Threshold"), dungeons=(crypt,))
session = GameSession.new(Party(members=[hero]), adventure, seed=7)
session.execute(EnterDungeon(dungeon_id="crypt"))

result = session.execute(MoveParty(direction=Direction.EAST))
print([event.code for event in result.events])
# ['exploration.party.moved']
table = [format_message(event) for event in result.events if event.visibility is Visibility.PLAYER]
print(table)
# ['The party moves to (1, 0), facing east.']

ALL_EVENT_CLASSES module-attribute

ALL_EVENT_CLASSES: tuple[type[Event], ...] = (*KERNEL_EVENT_CLASSES, *CRAWL_EVENT_CLASSES)

Every event class the library can emit: the kernel ones first, then the crawl ones.

This is the registry to walk when you are generating something from the whole event surface, such as client types, a documentation table, or a schema bundle. Each class carries its wire name in model_fields["event_type"].default and its code set in allowed_codes.

AnyEvent module-attribute

AnyEvent = Annotated[Union[*ALL_EVENT_CLASSES,], Field(discriminator='event_type')]

Any event the library can emit, as a union pydantic discriminates on event_type.

Use it to type a value that holds one event of no particular class, and hand it to a TypeAdapter to get a tagged-union JSON Schema for a client in another language. To parse one record, call parse_any_event instead: it skips an event type this version has no class for rather than raising on it.

from pydantic import TypeAdapter

from osrlib.crawl.events import AnyEvent

schema = TypeAdapter(AnyEvent).json_schema()
print(schema["discriminator"]["propertyName"])
# event_type

AdventureCompletedEvent

Bases: Event

The adventure is over and the party won.

Emitted by CompleteQuest when the quest that concludes the adventure completes, right after that quest's own QuestCompletedEvent, with the same beat. The session mode becomes victory, which is final: play commands are refused and nothing leaves it, so a front end can treat this as its closing screen.

A session that has already ended doesn't get an ending twice: a party that finishes the job after it has already fallen completes the quest and stays in game_over.

allowed_codes class-attribute

allowed_codes: frozenset[str] = frozenset({'session.adventure.completed'})

The only message code this event uses.

event_type class-attribute instance-attribute

event_type: Literal['adventure_completed'] = 'adventure_completed'

The wire discriminator, adventure_completed.

code class-attribute instance-attribute

code: str = 'session.adventure.completed'

The message code, always session.adventure.completed.

visibility class-attribute instance-attribute

Player visibility: it's the party's ending.

quest_id instance-attribute

quest_id: str

The quest that concluded the adventure.

name class-attribute instance-attribute

name: str = ''

That quest's display name. It defaults empty only so an event written by an older version still parses. The engine always fills it.

narrative class-attribute instance-attribute

narrative: str | None = None

The quest's completion beat, the same line its QuestCompletedEvent carried, or None.

AdventureXpAwardEvent

Bases: Event

The delve paid out: what the party earned and what each survivor takes.

Emitted by TravelToTown under the default ruleset, where experience is awarded for making it back. Each survivor's own XpAwardedEvent follows it, and a level gained follows that.

Treasure counts by what the party carried out compared with what it carried in, so goods still lying in the dungeon are worth nothing yet. A party that lost everyone awards nothing, because nobody came back to spend it.

allowed_codes class-attribute

allowed_codes: frozenset[str] = frozenset({'session.xp.adventure_award'})

The only message code this event uses.

event_type class-attribute instance-attribute

event_type: Literal['adventure_xp_award'] = 'adventure_xp_award'

The wire discriminator, adventure_xp_award.

code class-attribute instance-attribute

code: str = 'session.xp.adventure_award'

The message code, always session.xp.adventure_award.

visibility class-attribute instance-attribute

Player visibility: the award is the point of coming back.

monster_xp instance-attribute

monster_xp: int

What the defeated creatures were worth, added up across the whole delve.

treasure_xp instance-attribute

treasure_xp: int

What the recovered treasure was worth, one experience point per gold piece of value gained since the party left town, and never less than zero.

share instance-attribute

share: int

What each survivor receives: the total divided by the number of survivors, rounded down.

survivors instance-attribute

survivors: tuple[str, ...]

The members who made it back, in marching order. The dead count toward the treasure that came home but take no share.

BattleEndedEvent

Bases: Event

The battle is over: won, quit, or lost.

Emitted by ResolveBattleRound, and by EngageBattle when the opposition breaks before the first exchange.

A victory ends the encounter with it. A retreat may leave the party in a chase rather than clear of the fight. A defeat means nobody is left standing, and a GameOverEvent closes the same result.

allowed_codes class-attribute

allowed_codes: frozenset[str] = frozenset({'battle.ended.victory', 'battle.ended.fled', 'battle.ended.defeat'})

battle.ended.victory when no opposition is left fighting, battle.ended.fled when the party pulled out, and battle.ended.defeat when the party fell.

event_type class-attribute instance-attribute

event_type: Literal['battle_ended'] = 'battle_ended'

The wire discriminator, battle_ended.

visibility class-attribute instance-attribute

Player visibility: the fight is the party's own.

BattleRoundEvent

Bases: Event

A battle round began.

Emitted at the top of every ResolveBattleRound, before the declarations post and initiative is rolled. It's the marker a transcript can group the rest of the round's events under.

allowed_codes class-attribute

allowed_codes: frozenset[str] = frozenset({'battle.round.started'})

The only message code this event uses.

event_type class-attribute instance-attribute

event_type: Literal['battle_round'] = 'battle_round'

The wire discriminator, battle_round.

code class-attribute instance-attribute

code: str = 'battle.round.started'

The message code, always battle.round.started.

visibility class-attribute instance-attribute

Player visibility: everyone at the table knows which round it is.

round instance-attribute

round: int

Which round of this battle is starting, counting from one.

BattleStartedEvent

Bases: Event

Blows have been struck: the encounter became a battle.

Emitted when a fight opens, by EngageBattle when the party attacks, and on its own when the monsters do, which can happen the moment an encounter opens, when a parley goes wrong, when undead are presented with a holy symbol, or when a chase ends in capture.

The session mode becomes battle, where ResolveBattleRound is the only play command the session accepts. Positions are no longer cells: each monster group has a distance from the party, and closing or pulling back moves that number.

allowed_codes class-attribute

allowed_codes: frozenset[str] = frozenset({'battle.started'})

The only message code this event uses.

event_type class-attribute instance-attribute

event_type: Literal['battle_started'] = 'battle_started'

The wire discriminator, battle_started.

code class-attribute instance-attribute

code: str = 'battle.started'

The message code, always battle.started.

visibility class-attribute instance-attribute

Player visibility: the party is in it.

CharacterLeveledUpEvent

Bases: Event

A character crossed a threshold and gained a level.

Emitted immediately after that member's own XpAwardedEvent, whichever award crossed the threshold. A character gains at most one level per award.

allowed_codes class-attribute

allowed_codes: frozenset[str] = frozenset({'session.level.gained'})

The only message code this event uses.

event_type class-attribute instance-attribute

event_type: Literal['leveled_up'] = 'leveled_up'

The wire discriminator, leveled_up.

code class-attribute instance-attribute

code: str = 'session.level.gained'

The message code, always session.level.gained.

visibility class-attribute instance-attribute

Player visibility: the table has been waiting for this one.

character_id instance-attribute

character_id: str

The member who levelled.

level_before instance-attribute

level_before: int

The level held before the award.

level_after instance-attribute

level_after: int

The level held after it, one higher.

hp_gained instance-attribute

hp_gained: int

How many hit points were added, the die and the constitution adjustment together, or the flat bonus past the class's last Hit Die.

hp_roll instance-attribute

hp_roll: int | None

The hit die that was rolled, or None past the class's last Hit Die, where levels bring a flat bonus and no die.

con_applied instance-attribute

con_applied: bool

Whether the constitution adjustment was applied, which happens only when a die was rolled.

title instance-attribute

title: str | None

The class's title for the new level, or None past the printed list of titles.

CurseRevealedEvent

Bases: Event

A cursed item showed its true nature, and won't let go.

Emitted alongside ItemIdentifiedEvent the first time a cursed item is used or worn. From here the bearer is stuck with it until something removes the curse.

allowed_codes class-attribute

allowed_codes: frozenset[str] = frozenset({'items.curse.revealed'})

The only message code this event uses.

event_type class-attribute instance-attribute

event_type: Literal['curse_revealed'] = 'curse_revealed'

The wire discriminator, curse_revealed.

code class-attribute instance-attribute

code: str = 'items.curse.revealed'

The message code, always items.curse.revealed.

visibility class-attribute instance-attribute

Player visibility: the bearer finds out the hard way.

character_id instance-attribute

character_id: str

The member the curse has attached itself to.

instance_id instance-attribute

instance_id: str

The session-scoped id of the cursed item.

template_id instance-attribute

template_id: str

What the item is, as its magic item catalog id.

DetectionRolledEvent

Bases: Event

A referee-rolled detection die: a search, a listen, a lock, or a trap trigger.

Emitted alongside the player-facing result of Search, ListenAtDoor, PickLock, InspectTreasure, and RemoveTreasureTrap, and whenever a trap gets its chance to spring.

The die is rolled whether or not there's anything to find, so that a failure and an empty cell look the same from the table. That is why this event is referee visibility: showing it to players would leak the answer the roll was hiding. A referee front end, or an LLM running the game, reads it to know what actually happened.

allowed_codes class-attribute

allowed_codes: frozenset[str] = frozenset({'exploration.detection.rolled'})

The only message code this event uses.

event_type class-attribute instance-attribute

event_type: Literal['detection_rolled'] = 'detection_rolled'

The wire discriminator, detection_rolled.

code class-attribute instance-attribute

code: str = 'exploration.detection.rolled'

The message code, always exploration.detection.rolled.

visibility class-attribute instance-attribute

Referee visibility: the roll is the part B/X keeps behind the screen.

character_id class-attribute instance-attribute

character_id: str | None = None

The member who rolled, or None for a check nobody made, like a trap's own chance to go off.

kind instance-attribute

kind: str

What was being checked: "listening", one of the search kinds ("secret_doors", "room_traps", "construction"), "open_locks", "treasure_traps", or "trap_spring" for a trap's chance to fire.

chance instance-attribute

chance: int

The number the roll had to come in at or under. The listening, search, and trap-spring kinds are X-in-6 chances rolled on a d6. The thief skills open_locks and treasure_traps are percentages rolled on d100.

roll class-attribute instance-attribute

roll: int | None = None

What came up, or None when the chance was zero and no die was rolled, as for a character with no chance at all of noticing construction tricks.

passed instance-attribute

passed: bool

Whether the check succeeded. A failed check and a nothing-there cell are deliberately indistinguishable from the player's side.

DiceRolledEvent

Bases: Event

The referee rolled dice for something the rules don't cover.

Emitted by RollDice. The roll comes off the session's own adjudication stream, kept apart from the streams the rules use, so a referee rolling for weather or a rumour never shifts the dice a later attack or save would have drawn.

It's referee visibility: a hidden adjudication isn't automatically the table's to see. Show it to the players yourself when the ruling was made in the open.

allowed_codes class-attribute

allowed_codes: frozenset[str] = frozenset({'adjudication.dice_rolled'})

The only message code this event uses.

event_type class-attribute instance-attribute

event_type: Literal['dice_rolled'] = 'dice_rolled'

The wire discriminator, dice_rolled.

code class-attribute instance-attribute

code: str = 'adjudication.dice_rolled'

The message code, always adjudication.dice_rolled.

visibility class-attribute instance-attribute

Referee visibility: the referee decides what to share.

expression instance-attribute

expression: str

What was rolled, as the dice expression that was asked for, like "2d6+1".

total instance-attribute

total: int

The result, dice and modifier together.

rolls instance-attribute

rolls: tuple[int, ...]

Each die's own result, in roll order, so a transcript can show the dice rather than only the sum.

DoorEvent

Bases: Event

A door changed state, named by the cell it borders and the side it sits on.

Emitted by the door commands, OpenDoor, CloseDoor, ForceDoor, PickLock, and WedgeDoor, and by the commands that leave a cell or a level, because doors the party opened swing shut behind it. A referee's SetDoorState emits it too, at referee visibility, since a door set open from behind the screen isn't something the party watched happen.

A door belongs to the edge between two cells, so the same door can be named from either side. Redraw from x, y, and direction rather than tracking door identity yourself.

allowed_codes class-attribute

allowed_codes: frozenset[str] = frozenset(
    {
        "exploration.door.opened",
        "exploration.door.closed",
        "exploration.door.forced",
        "exploration.door.stuck",
        "exploration.door.wedged",
        "exploration.door.swung_shut",
        "exploration.door.unlocked",
    }
)

exploration.door.opened and .closed for the plain cases, .forced for a door shouldered open and .stuck for the attempt that failed, .unlocked for a lock picked, .wedged for a door spiked in place, and .swung_shut for a door the party opened closing behind it.

event_type class-attribute instance-attribute

event_type: Literal['door'] = 'door'

The wire discriminator, door.

visibility class-attribute instance-attribute

Player visibility by default. The referee's SetDoorState overrides it to referee.

x instance-attribute

x: int

The column of the cell the door edge is named from.

y instance-attribute

y: int

The row of the cell the door edge is named from.

direction instance-attribute

direction: str

Which side of that cell the door is on, as a lowercase Direction value.

character_id class-attribute instance-attribute

character_id: str | None = None

The member who acted, for a force, a stuck attempt, or a picked lock, and None when the party acted as one or when nobody did, as with a door swinging shut.

narrative class-attribute instance-attribute

narrative: str | None = None

The success text the author wrote on the door's gate, when opening it satisfied one, else None. Content rather than engine prose: the event still has its code and its facts, and the default formatter appends this line after the templated one.

EncounterEndedEvent

Bases: Event

The encounter is over, however it went.

Emitted once the last group has been dealt with: beaten, evaded, escaped from, or driven off. The session goes back to exploring, and the clock owes at least one full turn, so time passes with this event even when the fight was short.

allowed_codes class-attribute

allowed_codes: frozenset[str] = frozenset({'encounter.ended'})

The only message code this event uses.

event_type class-attribute instance-attribute

event_type: Literal['encounter_ended'] = 'encounter_ended'

The wire discriminator, encounter_ended.

code class-attribute instance-attribute

code: str = 'encounter.ended'

The message code, always encounter.ended. The ending is in outcome.

visibility class-attribute instance-attribute

Player visibility: the party knows the encounter is behind it.

outcome instance-attribute

outcome: str

How it ended: "victory" when the monsters were beaten, "evaded" when the party got away before or during a fight, "escaped" when a chase ran out, or "turned" when undead were driven off.

EncounterStartedEvent

Bases: Event

The party has met something, and here is what it sees.

Emitted when an encounter opens, whichever way it did: walking into a keyed area with MoveParty, EnterDungeon or UseStairs arriving on one, a wandering check, or a referee's SpawnMonsters or SpawnNpcParty. The session mode becomes encounter, where the party can talk, run, wait, or fight.

It contains only what the party can see: a name, a count, a distance. The dice behind the meeting are reported on SurpriseRolledEvent and on the reaction roll, both at referee visibility. The two surprise outcomes are here, because being caught off guard is something the party lives through.

allowed_codes class-attribute

allowed_codes: frozenset[str] = frozenset({'encounter.started'})

The only message code this event uses.

event_type class-attribute instance-attribute

event_type: Literal['encounter_started'] = 'encounter_started'

The wire discriminator, encounter_started.

code class-attribute instance-attribute

code: str = 'encounter.started'

The message code, always encounter.started.

visibility class-attribute instance-attribute

Player visibility: this is the moment the table is told what walked in.

monster_name instance-attribute

monster_name: str

The label of the first group in the encounter, as the party would name it. An NPC adventuring party appears under a label like "Basic Adventurers", with its roster kept behind the screen.

count instance-attribute

count: int

How many creatures there are across every group in the encounter.

distance_feet instance-attribute

distance_feet: int

How far away they are, in feet, when the encounter opens. Battle starts from this distance and closes from there.

party_surprised class-attribute instance-attribute

party_surprised: bool = False

Whether the party was caught off guard, which costs it the first beat of the fight.

monsters_surprised class-attribute instance-attribute

monsters_surprised: bool = False

Whether the monsters were caught off guard, which gives the party a free round if the fight starts.

EvasionEvent

Bases: Event

The party tried to get away, and either did or has a pursuit on its hands.

Emitted by Evade. Getting clear at once ends the encounter there. Otherwise a chase begins, and its beats arrive as PursuitEvents, starting in this same result.

allowed_codes class-attribute

allowed_codes: frozenset[str] = frozenset({'encounter.evasion.succeeded', 'encounter.evasion.pursuit'})

encounter.evasion.succeeded when the party is away clean, encounter.evasion.pursuit when something gives chase.

event_type class-attribute instance-attribute

event_type: Literal['evasion'] = 'evasion'

The wire discriminator, evasion.

visibility class-attribute instance-attribute

Player visibility: the party knows whether it's being followed.

ExhaustionEvent

Bases: Event

The party ran itself ragged, or has rested long enough to recover.

Running flat out for a long chase costs a party 2 on its attack and damage rolls and makes it 2 easier to hit, until it rests. Emitted by Evade and Wait when a chase runs its full length, and recovered by Rest once three turns of rest have been credited.

allowed_codes class-attribute

allowed_codes: frozenset[str] = frozenset({'encounter.exhaustion.gained', 'encounter.exhaustion.recovered'})

encounter.exhaustion.gained when the running catches up with the party, encounter.exhaustion.recovered when enough rest clears it.

event_type class-attribute instance-attribute

event_type: Literal['exhaustion'] = 'exhaustion'

The wire discriminator, exhaustion.

visibility class-attribute instance-attribute

Player visibility: the penalty is on the party's own sheets.

FatigueEvent

Bases: Event

The party picked up the unrested penalty, or shook it off.

B/X asks a party to rest one turn in every six while it's in a dungeon. A party that doesn't gets a penalty until it does, and these two codes are when the penalty lands and when it lifts.

Emitted while exploring, by any command that crosses a turn boundary, and recovered by Rest.

allowed_codes class-attribute

allowed_codes: frozenset[str] = frozenset({'exploration.fatigue.gained', 'exploration.fatigue.recovered'})

exploration.fatigue.gained when the party misses its rest, exploration.fatigue.recovered when a rest clears it.

event_type class-attribute instance-attribute

event_type: Literal['fatigue'] = 'fatigue'

The wire discriminator, fatigue.

visibility class-attribute instance-attribute

Player visibility: the party feels it, and the penalty is on their sheets.

FlagSetEvent

Bases: Event

A session flag was written.

Emitted by the referee's SetFlag. Flags are the game's own memory: an adventure's triggers and gates read them through a FlagEqualsCondition, and a game can keep whatever else it wants there.

It's referee visibility, because what the game is keeping track of isn't part of the fiction the party is in.

allowed_codes class-attribute

allowed_codes: frozenset[str] = frozenset({'session.flag.set'})

The only message code this event uses.

event_type class-attribute instance-attribute

event_type: Literal['flag_set'] = 'flag_set'

The wire discriminator, flag_set.

code class-attribute instance-attribute

code: str = 'session.flag.set'

The message code, always session.flag.set.

visibility class-attribute instance-attribute

Referee visibility: the wiring behind the game stays behind the screen.

key instance-attribute

key: str

Which flag was written.

value instance-attribute

value: str | int | bool

What it was set to. Writing an existing key replaces its value.

GameOverEvent

Bases: Event

Every party member is dead and the session has ended.

Emitted by whatever command's events killed the last member: a lost battle, a trap, a fall down a chute, starvation, a poison that finished someone while the referee was moving the clock. It closes that command's result, and the session mode becomes game_over.

Play commands are refused from there. A referee can still act, and PlaceParty is the way out, because carrying the fallen back to town is the first step of a revival.

allowed_codes class-attribute

allowed_codes: frozenset[str] = frozenset({'session.game_over'})

The only message code this event uses.

event_type class-attribute instance-attribute

event_type: Literal['game_over'] = 'game_over'

The wire discriminator, game_over.

code class-attribute instance-attribute

code: str = 'session.game_over'

The message code, always session.game_over.

visibility class-attribute instance-attribute

Player visibility: it's the party's ending.

reason instance-attribute

reason: str

Why the session ended, as a short phrase. Every ending reports the party falling. What killed them is in the events just before it.

GroupMovedEvent

Bases: Event

A monster group's distance from the party changed.

Emitted by ResolveBattleRound whenever the range track moves: the party closing on a group or backing away from every group, monsters closing to strike, and a broken group running for the exit.

Distance decides what can reach what, so this is the event a battle screen redraws its ranks from.

allowed_codes class-attribute

allowed_codes: frozenset[str] = frozenset({'battle.group.moved'})

The only message code this event uses.

event_type class-attribute instance-attribute

event_type: Literal['group_moved'] = 'group_moved'

The wire discriminator, group_moved.

code class-attribute instance-attribute

code: str = 'battle.group.moved'

The message code, always battle.group.moved.

visibility class-attribute instance-attribute

Player visibility: the party watches them come on or draw off.

group_id instance-attribute

group_id: str

Which group moved, as its encounter group id.

distance_feet instance-attribute

distance_feet: int

How far that group now stands from the party, in feet, after the move. Melee happens at the track's shortest step.

HealingPurchasedEvent

Bases: Event

A temple service was paid for and cast.

Emitted by PurchaseHealing in town, followed by the kernel events of the spell itself. The temple charges the party, so payers and payments_gp say which purses covered the fee and what each one put in.

allowed_codes class-attribute

allowed_codes: frozenset[str] = frozenset({'town.healing.purchased'})

The only message code this event uses.

event_type class-attribute instance-attribute

event_type: Literal['healing_purchased'] = 'healing_purchased'

The wire discriminator, healing_purchased.

code class-attribute instance-attribute

code: str = 'town.healing.purchased'

The message code, always town.healing.purchased.

visibility class-attribute instance-attribute

Player visibility: the party bought it.

character_id instance-attribute

character_id: str

The member the service was cast on. Their purse is charged first, but the party covers whatever is left, so read payers for who actually paid.

service instance-attribute

service: str

Which service was bought, as the key the town's price list uses.

cost_gp instance-attribute

cost_gp: int

What it cost, in gold pieces.

payers class-attribute instance-attribute

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

Whose purses paid, in the order they were charged: the treated member first, then the rest of the party in marching order, dead members included. A purse pays in whole gold pieces, as much of the outstanding fee as its gold covers, so the last purse charged pays what is left and the purses behind it are never opened. A member whose purse stayed shut is absent. It is empty on an event loaded from a save written before the field existed.

payments_gp class-attribute instance-attribute

payments_gp: tuple[int, ...] = ()

What each purse in payers paid, in gold pieces and in the same order. The entries sum to cost_gp.

HoardGeneratedEvent

Bases: Event

Treasure was rolled up and placed, before anyone has found it.

Emitted when the party first enters an area whose author declared treasure, and when a keyed encounter's monsters bring their lair hoard with them. The goods go into a cache the party has to find and open with TakeTreasure.

It's referee visibility, and it itemizes everything: telling the players would be telling them what is in the room. A referee front end, or an LLM running the game, reads it to know what is there.

allowed_codes class-attribute

allowed_codes: frozenset[str] = frozenset({'treasure.hoard.generated'})

The only message code this event uses.

event_type class-attribute instance-attribute

event_type: Literal['hoard_generated'] = 'hoard_generated'

The wire discriminator, hoard_generated.

code class-attribute instance-attribute

code: str = 'treasure.hoard.generated'

The message code, always treasure.hoard.generated.

visibility class-attribute instance-attribute

Referee visibility: the contents are the answer to a question the party hasn't asked yet.

cache_ref instance-attribute

cache_ref: str

The id of the cache the treasure went into, allocated by the session as cache-NNNN. The party reaches it through the cell it sits on, not through this id.

treasure_types class-attribute instance-attribute

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

The treasure-type letters that were rolled, one entry per roll, so a hoard rolled from two letters lists both.

coins_gp_value class-attribute instance-attribute

coins_gp_value: int = 0

The coins in the hoard, as their value in gold pieces.

valuable_ids class-attribute instance-attribute

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

The session-scoped instance ids of the gems and jewellery in the hoard.

magic_item_ids class-attribute instance-attribute

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

The session-scoped instance ids of the magic items in the hoard.

ItemAcquiredEvent

Bases: Event

Items or coins landed in a character's inventory.

Emitted by TakeTreasure once per carrier who took a share, by PurchaseEquipment in town, and by the referee's GrantItem and GrantCoins.

It reports what changed hands, not what the character now carries. Read the inventory itself from GameSession.view when you need the full sheet.

allowed_codes class-attribute

allowed_codes: frozenset[str] = frozenset({'exploration.item.acquired'})

The only message code this event uses.

event_type class-attribute instance-attribute

event_type: Literal['item_acquired'] = 'item_acquired'

The wire discriminator, item_acquired.

code class-attribute instance-attribute

code: str = 'exploration.item.acquired'

The message code, always exploration.item.acquired.

visibility class-attribute instance-attribute

Player visibility: the party knows what it picked up.

character_id instance-attribute

character_id: str

The member whose pack the goods went into.

item_ids class-attribute instance-attribute

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

What was acquired, one entry per item: a catalog id for mundane gear, repeated when several of the same thing arrived, and a session-scoped instance id for a valuable or a magic item, so an unidentified item's true nature stays hidden.

coins_gp_value class-attribute instance-attribute

coins_gp_value: int = 0

The coins acquired, converted to their value in gold pieces, and zero when only items changed hands.

origin class-attribute instance-attribute

origin: str | None = None

Where the goods came from: "treasure" for a share of a haul taken with TakeTreasure, "purchase" for gear bought with PurchaseEquipment, and "grant" for a referee's GrantItem or GrantCoins. Every command that emits this event fills it, so it is None only on an event loaded from a save written before the field existed.

ItemConsumedEvent

Bases: Event

One carried item was used up: a toll paid, a spike driven home.

Emitted when a gate's condition takes the item it names, which happens on OpenDoor, ForceDoor, and UseStairs, and by WedgeDoor for the iron spike it drives.

It says the item is gone. It isn't the event for a potion drunk or a scroll read, which are ItemUsedEvent.

allowed_codes class-attribute

allowed_codes: frozenset[str] = frozenset({'exploration.item.consumed'})

The only message code this event uses.

event_type class-attribute instance-attribute

event_type: Literal['item_consumed'] = 'item_consumed'

The wire discriminator, item_consumed.

code class-attribute instance-attribute

code: str = 'exploration.item.consumed'

The message code, always exploration.item.consumed.

visibility class-attribute instance-attribute

Player visibility: the party sees what it spent.

character_id instance-attribute

character_id: str

The member whose pack the item came out of.

item_id instance-attribute

item_id: str

What was consumed: the catalog id for mundane gear, and the session-scoped instance id for a magic item, never its template id, so an unidentified item's identity never reaches a player-visible event.

ItemIdentifiedEvent

Bases: Event

A magic item gave itself away, and the party now knows what it is.

Emitted the first time an item is used in a way that reveals it, which happens inside UseItem, EquipItem, ResolveBattleRound, and the referee's IdentifyItem.

Before this, the item's instance_id is all any player-visible event named. After it, the party can be shown the template's name.

allowed_codes class-attribute

allowed_codes: frozenset[str] = frozenset({'items.item.identified'})

The only message code this event uses.

event_type class-attribute instance-attribute

event_type: Literal['item_identified'] = 'item_identified'

The wire discriminator, item_identified.

code class-attribute instance-attribute

code: str = 'items.item.identified'

The message code, always items.item.identified.

visibility class-attribute instance-attribute

Player visibility: this is the moment the party learns what it has.

instance_id instance-attribute

instance_id: str

The session-scoped id of the item, the same id the earlier events used.

template_id instance-attribute

template_id: str

What it turned out to be, as its magic item catalog id.

ItemUsedEvent

Bases: Event

A magic item was used: a potion drunk, a scroll read, a device fired.

Emitted by UseItem out of combat and by ResolveBattleRound for an item used in a fight. Whatever the item does follows in the same result as kernel events.

Using an item for the first time is what identifies it, so an ItemIdentifiedEvent and possibly a CurseRevealedEvent come just before this one. Firing a device that has nothing left in it is a rejection (items.device.inert) rather than an event, because it costs the party nothing. Charges never appear on any event: how many uses an item has left is the referee's to know.

allowed_codes class-attribute

allowed_codes: frozenset[str] = frozenset(
    {"items.potion.drunk", "items.potion.mixed", "items.scroll.read", "items.scroll.cursed", "items.device.activated"}
)

items.potion.drunk for a potion taken on its own, items.potion.mixed when it meets another still running, which loses both and lays the drinker out for three turns, items.scroll.read for a scroll, items.scroll.cursed for one whose script was baneful, and items.device.activated for a rod, staff, wand, or other device.

event_type class-attribute instance-attribute

event_type: Literal['item_used'] = 'item_used'

The wire discriminator, item_used.

visibility class-attribute instance-attribute

Player visibility: the party watched it happen.

character_id instance-attribute

character_id: str

The member who used the item.

instance_id instance-attribute

instance_id: str

The session-scoped id of the item used, never its template id, so an item the party hasn't identified keeps its secret.

manual class-attribute instance-attribute

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

The item's printed text, for the items whose effect the engine doesn't resolve, like a treasure map or a curse the game narrates. Empty when the engine resolved the effect itself. Show these lines to the table and adjudicate them yourself.

ItemsDroppedEvent

Bases: Event

Items or coins were dropped, onto the party's cell or behind it as bait.

Emitted by DropItems, and by Evade when the party throws treasure or food to a pursuer to buy time.

What lands on a cell goes into that cell's drop pile, which TakeTreasure can pick back up. What is scattered during a flight is gone.

allowed_codes class-attribute

allowed_codes: frozenset[str] = frozenset({'exploration.item.dropped'})

The only message code this event uses.

event_type class-attribute instance-attribute

event_type: Literal['items_dropped'] = 'items_dropped'

The wire discriminator, items_dropped.

code class-attribute instance-attribute

code: str = 'exploration.item.dropped'

The message code, always exploration.item.dropped.

visibility class-attribute instance-attribute

Player visibility: the party knows what it let go of.

character_id instance-attribute

character_id: str

The member who dropped them.

item_ids class-attribute instance-attribute

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

What was dropped, in the same id form the acquisition used: catalog ids for mundane gear, instance ids for valuables and magic items.

coins_gp_value class-attribute instance-attribute

coins_gp_value: int = 0

The coins dropped, as their value in gold pieces.

ItemsGivenEvent

Bases: Event

Items or coins passed from one party member to another.

Emitted by GiveItems. Update both characters on your inventory screen when it arrives: nothing enters or leaves the party, so the party's total is unchanged.

allowed_codes class-attribute

allowed_codes: frozenset[str] = frozenset({'exploration.item.given'})

The only message code this event uses.

event_type class-attribute instance-attribute

event_type: Literal['items_given'] = 'items_given'

The wire discriminator, items_given.

code class-attribute instance-attribute

code: str = 'exploration.item.given'

The message code, always exploration.item.given.

visibility class-attribute instance-attribute

Player visibility: the party arranged the handover.

character_id instance-attribute

character_id: str

The member who handed the goods over.

recipient_id instance-attribute

recipient_id: str

The member who took them.

item_ids class-attribute instance-attribute

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

What was handed over, as catalog ids for mundane gear and instance ids for valuables and magic items.

coins_gp_value class-attribute instance-attribute

coins_gp_value: int = 0

The coins handed over, as their value in gold pieces.

ItemsLeftBehindEvent

Bases: Event

Treasure the party could not carry, left lying where it was found.

Emitted by TakeTreasure when the haul is heavier than the carriers' remaining capacity.

Nothing is destroyed. The remainder goes into the drop pile on the party's cell, so a party that comes back lighter can take another TakeTreasure and get the rest.

allowed_codes class-attribute

allowed_codes: frozenset[str] = frozenset({'exploration.item.left_behind'})

The only message code this event uses.

event_type class-attribute instance-attribute

event_type: Literal['items_left_behind'] = 'items_left_behind'

The wire discriminator, items_left_behind.

code class-attribute instance-attribute

code: str = 'exploration.item.left_behind'

The message code, always exploration.item.left_behind.

visibility class-attribute instance-attribute

Player visibility: the party can see the pile it's walking away from.

item_ids class-attribute instance-attribute

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

What stayed behind, as catalog ids for mundane gear and instance ids for valuables and magic items.

coins_gp_value class-attribute instance-attribute

coins_gp_value: int = 0

The coins left behind, as their value in gold pieces.

JournalEntryAddedEvent

Bases: Event

A beat was written into the session journal.

Emitted by AddJournalEntry. The journal is the party's own record of the adventure, appended in order and never rewritten, and it's part of what GameSession.view shows a player.

It isn't the only event a growing journal produces. A quest beat appends its entry and reports itself through its own lifecycle event instead, so the table isn't told the same line twice. Read the whole journal from the view, and read these events to know when a line arrived.

allowed_codes class-attribute

allowed_codes: frozenset[str] = frozenset({'session.journal.entry_added'})

The only message code this event uses.

event_type class-attribute instance-attribute

event_type: Literal['journal_entry_added'] = 'journal_entry_added'

The wire discriminator, journal_entry_added.

code class-attribute instance-attribute

code: str = 'session.journal.entry_added'

The message code, always session.journal.entry_added.

visibility class-attribute instance-attribute

Player visibility: the journal is written for the table.

text instance-attribute

text: str

The beat as it was written. Content the game or the adventure supplied, not prose the engine wrote.

rounds instance-attribute

rounds: int

Where the clock stood when the beat landed, in rounds since the session began. The stored entry has the same stamp.

LightEvent

Bases: Event

A light source was lit, went out, failed to catch, or burned away.

Emitted by LightSource, ExtinguishSource, and UseItem for an item that glows. The session also emits the expiry form whenever the clock runs a light out, which can happen inside any command that passes time.

Light gates most of exploration: searching, reading, and seeing at all need it unless a character has infravision. Read the party's current state from GameSession.party_light rather than adding these events up.

allowed_codes class-attribute

allowed_codes: frozenset[str] = frozenset(
    {"exploration.light.lit", "exploration.light.extinguished", "exploration.light.failed", "exploration.light.expired"}
)

exploration.light.lit when a source catches, .extinguished when it's put out on purpose, .failed when a tinder box doesn't catch, and .expired when a burning source runs out on the clock.

event_type class-attribute instance-attribute

event_type: Literal['light'] = 'light'

The wire discriminator, light.

visibility class-attribute instance-attribute

Player visibility: the party watches the light go. The ledger's own expiry record behind it is referee visibility, and the session translates it into this player-facing form.

character_id class-attribute instance-attribute

character_id: str | None = None

The member carrying the source, or None when the light belongs to no one in the party.

source instance-attribute

source: str

What is burning: "torch", "lantern", "oil" for a lit pool, "sword" for a blade that glows, or the effect kind for a light cast as a spell.

ListenedEvent

Bases: Event

Someone listened at a door, and either heard something or heard nothing.

Emitted by ListenAtDoor, after the referee-visibility DetectionRolledEvent that reports the die.

Silence is ambiguous, and it's meant to stay that way. Undead make no noise, and the roll happens whether or not anything is on the other side, so exploration.listen.silent tells the party nothing about what is there. Render it as an empty result, not as an all-clear.

allowed_codes class-attribute

allowed_codes: frozenset[str] = frozenset({'exploration.listen.heard', 'exploration.listen.silent'})

exploration.listen.heard when noise came through, exploration.listen.silent when none did.

event_type class-attribute instance-attribute

event_type: Literal['listened'] = 'listened'

The wire discriminator, listened.

visibility class-attribute instance-attribute

Player visibility: what a character heard is theirs to know, while the die behind it is not.

character_id instance-attribute

character_id: str

The member who listened.

direction instance-attribute

direction: str

The side of the party's cell that was listened at, as a lowercase Direction value.

LocationEnteredEvent

Bases: Event

The party crossed into a new area, level, dungeon, or town.

Emitted whenever the party's location changes at one of those four scales: EnterDungeon on arrival at a dungeon, UseStairs on a level or dungeon change, MoveParty on stepping into a keyed area, TravelToTown on arriving back in town, and PlaceParty when a referee puts the party somewhere.

Which fields are filled depends on the scale, because an area id is unique only within its level: an area entry names the area, its level number, and its dungeon, a level or dungeon entry names the dungeon in location_id with the level number beside it, and a town entry names neither. A level or dungeon entry also says how the party got there, in via and, for a transition it took, transition_ref, so a line written from the event alone can say the party climbed rather than descended. Use it to swap the screen's header, and read the text the party can see from GameSession.view.

allowed_codes class-attribute

allowed_codes: frozenset[str] = frozenset({'exploration.location.entered'})

The only message code this event uses.

event_type class-attribute instance-attribute

event_type: Literal['location_entered'] = 'location_entered'

The wire discriminator, location_entered.

code class-attribute instance-attribute

code: str = 'exploration.location.entered'

The message code, always exploration.location.entered. The scale is in location_kind.

visibility class-attribute instance-attribute

Player visibility: arriving somewhere is the first thing the table is told.

location_kind instance-attribute

location_kind: str

Which scale was crossed: "area", "level", "dungeon", or "town".

location_id instance-attribute

location_id: str

What was entered: the area id for an area entry, the dungeon id for a level or dungeon entry, and "town" for the town.

level_number class-attribute instance-attribute

level_number: int | None = None

The level the party is on, for area, level, and dungeon entries, and None for town.

dungeon_id class-attribute instance-attribute

dungeon_id: str | None = None

The dungeon the area belongs to, filled on area entries only. The other kinds already name the dungeon in location_id.

narrative class-attribute instance-attribute

narrative: str | None = None

The success text the adventure's author wrote on the gate that was crossed, when there was one, else None. A gate is the condition an author puts on a transition, like a door that opens only for a key. This is content rather than prose the engine wrote: the event still has its code and its facts, and format_message appends this line after the templated one.

via class-attribute instance-attribute

via: str | None = None

How the party got there, on level and dungeon entries: the kind of the transition it took ("stairs_down", "stairs_up", "trapdoor", or "chute"), "trap" for a trap that dropped the party through the floor, "entrance" for EnterDungeon, and "placed" for PlaceParty. It is None on area and town entries, and on an event loaded from a save written before the field existed.

transition_ref class-attribute instance-attribute

transition_ref: str | None = None

The cell of the authored transition the party took, as cell_ref gives it, so a consumer can find the TransitionSpec and the gate on it. Filled on a level or dungeon entry made through UseStairs, None otherwise.

MonsterDefeatedEvent

Bases: Event

One monster is out of the fight, and here is what it was worth.

Emitted once per defeated creature when the encounter concludes, which follows the last ResolveBattleRound or an escape that leaves the fight behind.

These are the entries the adventure's experience award adds up, and the award itself arrives later, as AdventureXpAwardEvent on the trip back to town, or at once when the ruleset awards immediately.

allowed_codes class-attribute

allowed_codes: frozenset[str] = frozenset({'battle.monster.defeated'})

The only message code this event uses.

event_type class-attribute instance-attribute

event_type: Literal['monster_defeated'] = 'monster_defeated'

The wire discriminator, monster_defeated.

code class-attribute instance-attribute

code: str = 'battle.monster.defeated'

The message code, always battle.monster.defeated.

visibility class-attribute instance-attribute

Player visibility: the party sees them fall or flee.

monster_id instance-attribute

monster_id: str

The session id of the creature that was defeated.

template_id instance-attribute

template_id: str

What it was: a monster catalog id, or "npc:<class id>" for a defeated NPC adventurer.

outcome instance-attribute

outcome: str

How it went out: "slain", or "routed" when it fled or was turned.

xp instance-attribute

xp: int

What it's worth: the monster catalog's printed award, or the level-based award for an NPC adventurer.

MonsterFledEvent

Bases: Event

A monster group broke and ran.

Emitted by ResolveBattleRound when a group fails a morale check, and at the opening of a battle for a group whose morale is so low it never fights at all. A running group keeps moving away each round and is gone once it's far enough out. Its members still count as defeated for the adventure's experience award.

allowed_codes class-attribute

allowed_codes: frozenset[str] = frozenset({'battle.side.fled'})

The only message code this event uses.

event_type class-attribute instance-attribute

event_type: Literal['monster_fled'] = 'monster_fled'

The wire discriminator, monster_fled.

visibility class-attribute instance-attribute

Player visibility: the party sees them break.

group_id instance-attribute

group_id: str

Which group broke, as its encounter group id.

MonstersLeftBehindEvent

Bases: Event

A group that ran left its helpless members lying where they were.

Emitted by ResolveBattleRound when a broken group has members who cannot run, because they are asleep, paralysed, or held.

The runners keep the original group and go on fleeing, and the ones left behind become a new group at the distance the side broke from, so the party can finish them, take what they carry, or walk past.

allowed_codes class-attribute

allowed_codes: frozenset[str] = frozenset({'battle.group.left_behind'})

The only message code this event uses.

event_type class-attribute instance-attribute

event_type: Literal['monsters_left_behind'] = 'monsters_left_behind'

The wire discriminator, monsters_left_behind.

code class-attribute instance-attribute

code: str = 'battle.group.left_behind'

The message code, always battle.group.left_behind.

visibility class-attribute instance-attribute

Player visibility: the party can see who was abandoned.

group_id instance-attribute

group_id: str

The new group the helpless members were put into.

source_group_id instance-attribute

source_group_id: str

The group that ran off without them.

count instance-attribute

count: int

How many were left behind.

MonstersSpawnedEvent

Bases: Event

Monsters were put into the session by the referee.

Emitted by SpawnMonsters, before the encounter that fields them opens in the same result.

It's referee visibility and contains ids rather than a description. The party learns what walked in from EncounterStartedEvent.

allowed_codes class-attribute

allowed_codes: frozenset[str] = frozenset({'session.monsters.spawned'})

The only message code this event uses.

event_type class-attribute instance-attribute

event_type: Literal['monsters_spawned'] = 'monsters_spawned'

The wire discriminator, monsters_spawned.

code class-attribute instance-attribute

code: str = 'session.monsters.spawned'

The message code, always session.monsters.spawned.

visibility class-attribute instance-attribute

Referee visibility: this is bookkeeping, not a moment in the fiction.

template_id instance-attribute

template_id: str

What was spawned, as a monster catalog id.

monster_ids instance-attribute

monster_ids: tuple[str, ...]

The session ids of the new instances, in spawn order. They are what every later event about those creatures names.

NoteRecordedEvent

Bases: Event

A referee note was recorded, and no game state changed.

Emitted by RecordNote. Games use it to leave a machine-written note in the log, like a consequence that could not be applied, and referees use it for their own margin notes.

allowed_codes class-attribute

allowed_codes: frozenset[str] = frozenset({'session.note.recorded'})

The only message code this event uses.

event_type class-attribute instance-attribute

event_type: Literal['note_recorded'] = 'note_recorded'

The wire discriminator, note_recorded.

code class-attribute instance-attribute

code: str = 'session.note.recorded'

The message code, always session.note.recorded.

visibility class-attribute instance-attribute

Referee visibility: a note is for the person running the game.

text instance-attribute

text: str

The note as it was written.

NpcPartySpawnedEvent

Bases: Event

An NPC adventuring party was rolled up and put on the board.

Emitted by the referee's SpawnNpcParty and by a wandering roll that comes up adventurers, before the encounter opens.

It's referee visibility and contains the whole roster. What the party sees is the EncounterStartedEvent, which names them as adventurers and gives a count. Their classes and levels are something to find out by talking or by fighting.

allowed_codes class-attribute

allowed_codes: frozenset[str] = frozenset({'encounter.npc_party.spawned'})

The only message code this event uses.

event_type class-attribute instance-attribute

event_type: Literal['npc_party_spawned'] = 'npc_party_spawned'

The wire discriminator, npc_party_spawned.

code class-attribute instance-attribute

code: str = 'encounter.npc_party.spawned'

The message code, always encounter.npc_party.spawned.

visibility class-attribute instance-attribute

Referee visibility: the roster is what the party doesn't get to read off a sheet.

party_kind instance-attribute

party_kind: str

Which table the party was rolled from: "basic" or "expert".

npc_ids instance-attribute

npc_ids: tuple[str, ...]

The session ids of its members, in roster order. The other three tuples line up with this one.

class_ids instance-attribute

class_ids: tuple[str, ...]

Each member's class, as a class catalog id.

levels instance-attribute

levels: tuple[int, ...]

Each member's level.

alignment instance-attribute

alignment: str

The party's alignment, as a lowercase Alignment value. It decides how they are played more than how they roll.

ObjectiveCompletedEvent

Bases: Event

One objective of a quest is done.

Emitted by CompleteObjective. Completing an objective also reveals it, so an objective the party finished before anyone announced it arrives here first and needs no separate reveal.

allowed_codes class-attribute

allowed_codes: frozenset[str] = frozenset({'session.quest.objective_completed'})

The only message code this event uses.

event_type class-attribute instance-attribute

event_type: Literal['objective_completed'] = 'objective_completed'

The wire discriminator, objective_completed.

code class-attribute instance-attribute

code: str = 'session.quest.objective_completed'

The message code, always session.quest.objective_completed.

visibility class-attribute instance-attribute

Player visibility: progress belongs to the party.

quest_id instance-attribute

quest_id: str

The quest the objective belongs to.

quest_name class-attribute instance-attribute

quest_name: str = ''

The quest's display name, resolved when the event is made. It defaults empty only so an event written by an older version still parses. The engine always fills it.

objective_id instance-attribute

objective_id: str

Which objective was completed.

name class-attribute instance-attribute

name: str = ''

The objective's display label: the name its author wrote, or its id when the adventure wrote none.

narrative class-attribute instance-attribute

narrative: str | None = None

The progress beat the author wrote, or None. The journal contains the same line.

ObjectiveRevealedEvent

Bases: Event

A hidden objective surfaced: the party can now be told what it's being asked for.

Emitted by RevealObjective. An objective the adventure didn't mark hidden is visible from the start and is never revealed.

allowed_codes class-attribute

allowed_codes: frozenset[str] = frozenset({'session.quest.objective_revealed'})

The only message code this event uses.

event_type class-attribute instance-attribute

event_type: Literal['objective_revealed'] = 'objective_revealed'

The wire discriminator, objective_revealed.

code class-attribute instance-attribute

code: str = 'session.quest.objective_revealed'

The message code, always session.quest.objective_revealed.

visibility class-attribute instance-attribute

Player visibility: the party is being told what to do.

quest_id instance-attribute

quest_id: str

The quest the objective belongs to.

quest_name class-attribute instance-attribute

quest_name: str = ''

The quest's display name, resolved when the event is made so a renderer holds no adventure to look it up in. It defaults empty only so an event written by an older version still parses. The engine always fills it.

objective_id instance-attribute

objective_id: str

Which objective, as the id the adventure gave it.

name class-attribute instance-attribute

name: str = ''

The objective's display label: the name its author wrote, or its id when the adventure wrote none. It defaults empty for the same parsing reason as quest_name.

narrative class-attribute instance-attribute

narrative: str | None = None

The offer beat the author wrote for this objective, or None. The journal contains the same line.

PartyMovedEvent

Bases: Event

The party moved a cell or turned in place, and here is where it now stands.

Emitted by MoveParty with the cell it stepped into, and by TurnParty with the unchanged cell and the new facing. It's what a first-person front end redraws from.

A move that a wall, a closed door, or the edge of the map stops is a rejection (exploration.move.blocked) rather than an event: walking into a wall changes nothing about the game, so nothing is reported.

allowed_codes class-attribute

allowed_codes: frozenset[str] = frozenset({'exploration.party.moved', 'exploration.party.turned'})

exploration.party.moved for a step into a new cell, exploration.party.turned for a turn on the spot.

event_type class-attribute instance-attribute

event_type: Literal['party_moved'] = 'party_moved'

The wire discriminator, party_moved.

visibility class-attribute instance-attribute

Player visibility: where the party stands is the party's own business.

x instance-attribute

x: int

The column the party is in after the command, counting from zero at the level's west edge.

y instance-attribute

y: int

The row the party is in after the command, counting from zero at the level's north edge.

facing instance-attribute

facing: str

The direction the party now faces, as the lowercase value of a Direction ("north", "east", "south", "west"). A move faces the way it went, so this changes on a step as well as on a turn.

ProvisionsEvent

Bases: Event

A day passed, and a character either ate and drank or went without.

Emitted once per living member per kind whenever the clock crosses a day boundary, which can happen inside any command that passes time, and most often inside a Rest.

Going short starts a deprivation count on that member. Whether that count brings a penalty depends on the ruleset option deprivation_penalties, described in the adaptations register, the page that lists where osrlib settles an ambiguous rule or supplies a default. In town nobody ever runs short.

allowed_codes class-attribute

allowed_codes: frozenset[str] = frozenset({'exploration.provisions.consumed', 'exploration.provisions.short'})

exploration.provisions.consumed when the day's food or water was there, exploration.provisions.short when it was not.

event_type class-attribute instance-attribute

event_type: Literal['provisions'] = 'provisions'

The wire discriminator, provisions.

visibility class-attribute instance-attribute

Player visibility: an empty pack is the party's problem to solve.

character_id instance-attribute

character_id: str

The member whose rations or water this was.

kind instance-attribute

kind: str

Which supply the event is about: "food" or "water". Each member gets one of each per day.

PursuitEvent

Bases: Event

One beat of a chase: the gap, a distraction taken, an escape, or a capture.

Emitted by Evade once the chase is on, and by Wait and DropItems for each further beat, which is how the party keeps running or throws something behind it.

A capture opens battle at once, and an escape ends the encounter. A chase that runs long enough tires the party out, which arrives as ExhaustionEvent.

allowed_codes class-attribute

allowed_codes: frozenset[str] = frozenset(
    {"encounter.pursuit.round", "encounter.pursuit.distracted", "encounter.pursuit.escaped", "encounter.pursuit.caught"}
)

encounter.pursuit.round for a beat where the chase goes on, .distracted when dropped treasure or food stops the pursuers, .escaped when the party gets clear, and .caught when the pursuers close to arm's length and battle opens.

event_type class-attribute instance-attribute

event_type: Literal['pursuit'] = 'pursuit'

The wire discriminator, pursuit.

visibility class-attribute instance-attribute

Player visibility: the party can see how close the chase is.

round instance-attribute

round: int

Which beat of the chase this is, counting from one.

gap_feet instance-attribute

gap_feet: int

How far ahead the party is, in feet, at the end of this beat. It never goes below zero, and at five feet or less the pursuers have caught up.

QuestActivatedEvent

Bases: Event

A quest came into play.

Emitted by ActivateQuest. A quest the adventure marked as standing from the start needs no activation and no event: it is active from the first command.

It's player visibility, because a job the party has taken on is theirs to know, while the clause that set it off stays behind the screen with the trigger and flag events.

allowed_codes class-attribute

allowed_codes: frozenset[str] = frozenset({'session.quest.activated'})

The only message code this event uses.

event_type class-attribute instance-attribute

event_type: Literal['quest_activated'] = 'quest_activated'

The wire discriminator, quest_activated.

code class-attribute instance-attribute

code: str = 'session.quest.activated'

The message code, always session.quest.activated.

visibility class-attribute instance-attribute

Player visibility: the party is being given the job.

quest_id instance-attribute

quest_id: str

Which quest, as the id the adventure gave it.

name instance-attribute

name: str

The quest's display name, so a renderer needs no copy of the adventure to show it.

narrative class-attribute instance-attribute

narrative: str | None = None

The offer beat the author wrote, or None when there's none. The same line is appended to the journal, so this event and that entry report one moment once.

QuestCompletedEvent

Bases: Event

A quest is finished.

Emitted by CompleteQuest. Whether the quest is done is the referee's ruling: the engine checks that the quest is active, not that every objective was completed.

Rewards a quest pays out arrive after this event, as the commands the game issues for them and their own events. When the quest is the one that concludes the adventure, an AdventureCompletedEvent follows in the same result.

allowed_codes class-attribute

allowed_codes: frozenset[str] = frozenset({'session.quest.completed'})

The only message code this event uses.

event_type class-attribute instance-attribute

event_type: Literal['quest_completed'] = 'quest_completed'

The wire discriminator, quest_completed.

code class-attribute instance-attribute

code: str = 'session.quest.completed'

The message code, always session.quest.completed.

visibility class-attribute instance-attribute

Player visibility: finishing the job is the party's news.

quest_id instance-attribute

quest_id: str

Which quest was completed.

name instance-attribute

name: str

The quest's display name.

narrative class-attribute instance-attribute

narrative: str | None = None

The completion beat the author wrote, or None. The journal contains the same line.

RestedEvent

Bases: Event

A rest finished, or was interrupted before it could.

Emitted by Rest. A completed rest clears the unrested-fatigue penalty, credits running exhaustion, and, for a full day, heals naturally. An interrupted one does none of that, because something wandered in.

allowed_codes class-attribute

allowed_codes: frozenset[str] = frozenset({'exploration.rest.rested', 'exploration.rest.interrupted'})

exploration.rest.rested when the rest ran to its end, exploration.rest.interrupted when a wandering encounter cut it short.

event_type class-attribute instance-attribute

event_type: Literal['rested'] = 'rested'

The wire discriminator, rested.

visibility class-attribute instance-attribute

Player visibility: the party knows whether it got its rest.

kind instance-attribute

kind: str

How long the party tried to rest: "turn" for the one-turn breather the dungeon rule calls for, "night", or "day".

SearchCompletedEvent

Bases: Event

A search of the party's cell finished, naming whatever it turned up.

Emitted by Search and by InspectTreasure once the roll has been made, after the referee-visibility DetectionRolledEvent.

An empty result means the searcher found nothing, which isn't the same as there being nothing: each character gets one attempt per cell per kind, and another character may still find it.

allowed_codes class-attribute

allowed_codes: frozenset[str] = frozenset({'exploration.search.found', 'exploration.search.nothing'})

exploration.search.found when found is non-empty, exploration.search.nothing otherwise.

event_type class-attribute instance-attribute

event_type: Literal['search_completed'] = 'search_completed'

The wire discriminator, search_completed.

visibility class-attribute instance-attribute

Player visibility: what the search turned up is the party's to act on.

character_id instance-attribute

character_id: str

The member who searched.

kind instance-attribute

kind: str

What was searched for: "secret_doors", "room_traps", "construction", or "treasure_traps" for a treasure feature inspected by a thief.

found class-attribute instance-attribute

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

What turned up, as references like "secret_door:north", "room_trap:<area id>", or "construction:<feature id>", and empty when nothing did. A room trap found through a door from the searched cell names the door's direction in a third segment, "room_trap:<area id>:<direction>", and one found inside its own area has none. A found secret door becomes passable, and a found trap no longer springs on the party.

SpellDeclaredEvent

Bases: Event

Somebody declared a spell, before anyone knows who acts first.

Emitted by ResolveBattleRound for each caster who declared one, party member or NPC alike, at the top of the round.

B/X has declarations posted before initiative on purpose: a caster who takes damage before their turn loses the spell, and the other side can act on knowing what is coming. The disruption itself arrives later in the round as a kernel event.

allowed_codes class-attribute

allowed_codes: frozenset[str] = frozenset({'battle.spell.declared'})

The only message code this event uses.

event_type class-attribute instance-attribute

event_type: Literal['spell_declared'] = 'spell_declared'

The wire discriminator, spell_declared.

code class-attribute instance-attribute

code: str = 'battle.spell.declared'

The message code, always battle.spell.declared.

visibility class-attribute instance-attribute

Player visibility: a declaration is made out loud at the table.

caster_id instance-attribute

caster_id: str

Who is casting: a party member's id, or an NPC adventurer's.

spell_id instance-attribute

spell_id: str

Which spell was declared, as its catalog id.

reversed class-attribute instance-attribute

reversed: bool = False

Whether the reversed form was declared, for a spell that has one.

StanceChangedEvent

Bases: Event

The monsters' attitude toward the party changed, as behavior the party can read.

Emitted when an encounter opens with its first reaction, when Parley talks the monsters into a different mood, when an uncertain stance resolves on the next beat, and when TurnUndead settles the matter by making the survivors hostile.

The 2d6 reaction roll behind it is a kernel event at referee visibility. What reaches the party is how the creatures are acting.

allowed_codes class-attribute

allowed_codes: frozenset[str] = frozenset({'encounter.stance.changed'})

The only message code this event uses.

event_type class-attribute instance-attribute

event_type: Literal['stance_changed'] = 'stance_changed'

The wire discriminator, stance_changed.

code class-attribute instance-attribute

code: str = 'encounter.stance.changed'

The message code, always encounter.stance.changed.

visibility class-attribute instance-attribute

Player visibility: behavior is visible even when the roll behind it isn't.

stance instance-attribute

stance: str

How the monsters are acting now, as a ReactionResult value: "attacks", "hostile", "uncertain", "indifferent", or "friendly". An attacking stance opens battle in the same result.

SurpriseRolledEvent

Bases: Event

One side's surprise die, rolled behind the screen.

Emitted twice when an encounter opens, once for each side, before EncounterStartedEvent reports the outcomes to the table.

A side that cannot be surprised doesn't roll: a party that already knows what is in the room, or monsters that heard the party coming or can see its light.

allowed_codes class-attribute

allowed_codes: frozenset[str] = frozenset({'encounter.surprise.rolled'})

The only message code this event uses.

event_type class-attribute instance-attribute

event_type: Literal['surprise_rolled'] = 'surprise_rolled'

The wire discriminator, surprise_rolled.

code class-attribute instance-attribute

code: str = 'encounter.surprise.rolled'

The message code, always encounter.surprise.rolled.

visibility class-attribute instance-attribute

Referee visibility: the die is the referee's, and the result reaches the table through the encounter event.

side instance-attribute

side: str

Which side rolled: "party" or "monsters".

threshold instance-attribute

threshold: int

The number on a d6 at or under which that side is surprised. It is 2 as a rule, and 3 for a party moving in the dark without infravision.

roll class-attribute instance-attribute

roll: int | None = None

The d6 that came up, or None when this side never had to roll.

surprised instance-attribute

surprised: bool

Whether this side was surprised.

TimeAdvancedEvent

Bases: Event

The referee moved the clock.

Emitted by AdvanceTime. The time passes with all its usual bookkeeping, so effect expiries, light burning out, and provisions for a day crossed all arrive in the same result, but no wandering check runs: a referee moving the clock decides for themselves what walks in.

allowed_codes class-attribute

allowed_codes: frozenset[str] = frozenset({'session.time.advanced'})

The only message code this event uses.

event_type class-attribute instance-attribute

event_type: Literal['time_advanced'] = 'time_advanced'

The wire discriminator, time_advanced.

code class-attribute instance-attribute

code: str = 'session.time.advanced'

The message code, always session.time.advanced.

visibility class-attribute instance-attribute

Referee visibility: the clock is the referee's instrument.

n instance-attribute

n: int

How many units were asked for.

unit instance-attribute

unit: str

Which unit, as a lowercase TimeUnit value: "round", "turn", or "day".

rounds_total instance-attribute

rounds_total: int

Where the clock now stands, in rounds since the session began. It's the same number GameSession.clock.rounds holds.

TrapEvent

Bases: Event

A trap did something the party can perceive: it fired, or was found, or was dealt with.

Emitted by the commands that can set a trap off or look for one: MoveParty, UseStairs and EnterDungeon, which run the arrival cell's entry checks the same way a step does, OpenDoor and ForceDoor, Search, TakeTreasure, InspectTreasure, and RemoveTreasureTrap. A trap that fires resolves at once, so its damage and saves follow in the same result as kernel events.

A trap the party doesn't know about that fails to fire produces no event here. Only its die goes into the referee-visibility DetectionRolledEvent, so an uneventful step looks like a step on safe ground.

What a find is worth depends on the kind of trap. A found room trap never springs, at its area's edge or at one of its doors: the party walks around the known pit and stands clear of the known blade, so it rolls no die and emits nothing further. A found treasure trap still rolls its 2-in-6 on every TakeTreasure until a thief takes it out with RemoveTreasureTrap, because finding a treasure trap is not defeating it. That is why exploration.trap.safe, a known trap's trigger resolving without springing, appears on a cache alone.

allowed_codes class-attribute

allowed_codes: frozenset[str] = frozenset(
    {"exploration.trap.sprung", "exploration.trap.safe", "exploration.trap.found", "exploration.trap.removed"}
)

exploration.trap.sprung when a trap goes off, .found when a search or inspection reveals one, .removed when a thief disarms one, and .safe when a trap the party already knows about got its chance and didn't fire, which only a treasure trap does.

event_type class-attribute instance-attribute

event_type: Literal['trap'] = 'trap'

The wire discriminator, trap.

visibility class-attribute instance-attribute

Player visibility: the party feels the trap go off, or sees the one it found.

trap_ref instance-attribute

trap_ref: str

Which trap this is, as "<dungeon id>:<level number>:<area or feature id>". The session records the same reference as found, sprung, or removed, so a trap is reported once and stays dealt with.

character_id class-attribute instance-attribute

character_id: str | None = None

The member who set it off, found it, or removed it, or None when the trap fired on the party as a whole.

direction class-attribute instance-attribute

direction: str | None = None

The direction of the door a room trap was found through, from the searched cell, as a Direction value. Set for that find alone: it is None on a room trap found inside its own area, on a treasure trap found on a cache, and on every code but exploration.trap.found.

TreasureSoldEvent

Bases: Event

Valuables were sold in town, and the coins are in the purse.

Emitted by SellTreasure. Gems and jewellery sell for their full listed value, which keeps one gold piece worth one experience point however treasure is converted.

allowed_codes class-attribute

allowed_codes: frozenset[str] = frozenset({'town.treasure.sold'})

The only message code this event uses.

event_type class-attribute instance-attribute

event_type: Literal['treasure_sold'] = 'treasure_sold'

The wire discriminator, treasure_sold.

code class-attribute instance-attribute

code: str = 'town.treasure.sold'

The message code, always town.treasure.sold.

visibility class-attribute instance-attribute

Player visibility: the party made the sale.

character_id instance-attribute

character_id: str

The member who sold them and now holds the coins.

instance_ids instance-attribute

instance_ids: tuple[str, ...]

The session-scoped ids of the valuables that were sold.

gp_value instance-attribute

gp_value: int

What they fetched, in gold pieces.

TriggerFiredEvent

Bases: Event

An authored trigger fired.

Emitted by MarkTriggerFired, every time, including a repeat of a trigger that has fired before: the session records that a trigger has fired at all, and these events are the record of each firing.

It's referee visibility, because which clause fired is the wiring behind the game. A beat written for the table goes in the journal, and arrives as JournalEntryAddedEvent or as one of the quest events.

allowed_codes class-attribute

allowed_codes: frozenset[str] = frozenset({'session.trigger.fired'})

The only message code this event uses.

event_type class-attribute instance-attribute

event_type: Literal['trigger_fired'] = 'trigger_fired'

The wire discriminator, trigger_fired.

code class-attribute instance-attribute

code: str = 'session.trigger.fired'

The message code, always session.trigger.fired.

visibility class-attribute instance-attribute

Referee visibility: trigger wiring is the game's own.

trigger_id instance-attribute

trigger_id: str

Which trigger fired, as the id the adventure gave it.

narrative class-attribute instance-attribute

narrative: str | None = None

The beat the author wrote for this firing, or None. Content rather than engine prose: the default formatter appends it after the templated line. It reaches a referee-visibility event, so put anything meant for the table in the journal instead.

WanderingCheckEvent

Bases: Event

The wandering-monster cadence came round and the referee rolled for it.

Emitted while the party is in a dungeon, by any command that crosses the turn the cadence lands on, most often MoveParty or Rest. When the check hits, the encounter opens in the same result and the command that was spending time stops there.

It's referee visibility because the party has no way of knowing a check was made, only of meeting what it produced.

allowed_codes class-attribute

allowed_codes: frozenset[str] = frozenset({'exploration.wandering.checked'})

The only message code this event uses.

event_type class-attribute instance-attribute

event_type: Literal['wandering_check'] = 'wandering_check'

The wire discriminator, wandering_check.

code class-attribute instance-attribute

code: str = 'exploration.wandering.checked'

The message code, always exploration.wandering.checked.

visibility class-attribute instance-attribute

Referee visibility: the check is made behind the screen, and only its result walks in.

chance instance-attribute

chance: int

The X-in-6 chance the check needed, after the level's own rate and any adjustment like the lower chance while resting.

roll class-attribute instance-attribute

roll: int | None = None

The d6 that was rolled, or None when the chance came out at zero and no die was rolled.

encounter instance-attribute

encounter: bool

Whether the check produced an encounter. When it did, the encounter's own events follow in the same result.

XpAwardedEvent

Bases: Event

One character was awarded experience.

Emitted wherever an award lands: inside the end-of-adventure award on TravelToTown, at each encounter's end when the ruleset awards immediately, and from the referee's AwardXP. When the award crosses a threshold, a CharacterLeveledUpEvent for the same member follows it at once.

allowed_codes class-attribute

allowed_codes: frozenset[str] = frozenset({'session.xp.awarded'})

The only message code this event uses.

event_type class-attribute instance-attribute

event_type: Literal['xp_awarded'] = 'xp_awarded'

The wire discriminator, xp_awarded.

code class-attribute instance-attribute

code: str = 'session.xp.awarded'

The message code, always session.xp.awarded.

visibility class-attribute instance-attribute

Player visibility: it goes on the character's sheet.

character_id instance-attribute

character_id: str

The member who received it.

award instance-attribute

award: int

The award as the session handed it over, before the character's own adjustment.

modified_award instance-attribute

modified_award: int

What was actually added, after the class's prime-requisite percentage, rounded down. This is the number to show beside the character.

level_after instance-attribute

level_after: int

The member's level once the award was applied.

parse_any_event

parse_any_event(data: Mapping[str, object]) -> Event | None

Rebuild one serialized event, kernel or crawl, skipping types this version doesn't know.

Call it on records that came out of an event's model_dump or out of a save's event log, such as a log you are replaying, a stream you received over a network, or a file you are analyzing. A session restored by load_game uses it for the event log it reads, keeping the raw record for anything it could not parse.

An event_type this version has no class for returns None rather than raising, so a log written by a newer engine still loads under an older one. Unknown fields on a known type are ignored for the same reason. What you get back is an instance of the matching class, which you can hand to format_message like any other event.

Parameters:

Name Type Description Default
data Mapping[str, object]

One event as a mapping, from model_dump (in either Python or JSON mode) or from parsed JSON.

required

Returns:

Type Description
Event | None

The event, or None when its event_type belongs to no class in

Event | None

Raises:

Type Description
ContentValidationError

If the event type is known but the payload doesn't fit it, like a record missing a required field. The message carries pydantic's own report.

Examples:

from osrlib.crawl.events import PartyMovedEvent, parse_any_event

event = PartyMovedEvent(code="exploration.party.moved", x=1, y=0, facing="east")
record = event.model_dump()
print(parse_any_event(record) == event)
# True

print(parse_any_event({"event_type": "teleported", "code": "exploration.party.teleported"}))
# None