Skip to content

osrlib.crawl.commands

The command set: typed models, the discriminated union, and the result envelope.

Commands are the write API: build one and pass it to GameSession.execute. CommandResult is the envelope execute returns: accepted, the kernel's Rejection models verbatim, and the events. Commands mirror the event conventions exactly: frozen pydantic models, a single-valued command_type Literal discriminator (snake_case, schema-stable, additive-only), an AnyCommand discriminated union, and parse_command returning None on unknown types.

Each command declares its legal session modes as an allowed_modes class attribute; the session rejects a wrong-mode command with session.command.wrong_mode. Referee commands are legal in every mode and are logged and replayed like any other. Every command class documents its contract in three sections: Modes: (the legal session modes), Rejections: (the rejection codes it can return), and Events: (what it emits when accepted).

AnyCommand module-attribute

AnyCommand = Annotated[Union[*ALL_COMMAND_CLASSES,], Field(discriminator='command_type')]

Any command, discriminated by command_type.

AdvanceTime

Bases: Command

Referee: advance the clock directly.

Referee commands are legal in every mode. Time passes with full bookkeeping — effect expiries, provisions on day boundaries — but no wandering cadence: the referee controls encounters.

Modes

town, exploring, encounter, battle, game_over

Rejections

None.

Events

The span's bookkeeping events (effect expiries and their player-facing light translations, provisions), then TimeAdvancedEvent with the total.

command_type class-attribute instance-attribute

command_type: Literal['advance_time'] = 'advance_time'

n class-attribute instance-attribute

n: int = Field(ge=0)

unit instance-attribute

unit: TimeUnit

AwardXP

Bases: Command

Referee: apply an XP award to one character, outside the adventure award.

Referee commands are legal in every mode. The award applies the prime-requisite modifier and can trigger level gains.

Modes

town, exploring, encounter, battle, game_over

Rejections
  • session.command.unknown_membercharacter_id names no party member.
Events

XpAwardedEvent with the award, the modified award, and the level after.

command_type class-attribute instance-attribute

command_type: Literal['award_xp'] = 'award_xp'

character_id instance-attribute

character_id: str

amount class-attribute instance-attribute

amount: int = Field(ge=0)

BattleDeclaration

Bases: BaseModel

One party member's declared action for a battle round.

attack names a target group and optionally a wielded weapon (None is unarmed); cast names the spell, mode, form, and targets; move is the range-track intent; use_item covers thrown splash items against a group. Turn undead resolves in the magic phase but is never disruptable — turning is a class ability, not a spell.

character_id instance-attribute

character_id: str

action instance-attribute

action: Literal['attack', 'cast', 'turn_undead', 'move', 'use_item', 'hold']

target_group_id class-attribute instance-attribute

target_group_id: str | None = None

weapon_id class-attribute instance-attribute

weapon_id: str | None = None

spell_id class-attribute instance-attribute

spell_id: str | None = None

spell_mode class-attribute instance-attribute

spell_mode: str | None = None

reversed class-attribute instance-attribute

reversed: bool = False

targets class-attribute instance-attribute

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

move class-attribute instance-attribute

move: Literal['close', 'withdraw', 'fighting_withdrawal', 'retreat'] | None = None

item_id class-attribute instance-attribute

item_id: str | None = None

CastSpell

Bases: Command

Cast a memorized spell outside battle (one round).

targets are entity ids, or cell: references for location-bound casts. In encounter mode a hostile cast is opened through EngageBattle and the first round's declarations instead; in battle, casting is a declaration kind.

Modes

town, exploring

Rejections
  • session.command.wrong_mode — an encounter or battle is underway, or the game is over.
  • session.command.unknown_membercharacter_id names no party member.
  • session.command.member_incapacitated — the member cannot act.
  • magic.cast.unknown_spellspell_id names no spell.
  • magic.cast.silenced_area — a silence effect covers the party's cell.
  • magic.cast.unknown_target — a target reference resolves to nothing.
  • magic.cast.not_memorized — no memorized copy (non-casters included).
  • magic.cast.caster_incapacitated, magic.cast.caster_restrained, magic.cast.anti_magic_shell — the caster cannot cast right now.
  • magic.cast.not_reversiblereversed on a spell with no reversed form.
  • magic.cast.unknown_modemode names no mode of the spell.
  • magic.cast.target_count — the wrong number of targets for the mode.
  • magic.cast.out_of_range — a target lies beyond the spell's range.
Events

SpellCastEvent plus the spell's own resolution — saving throws, damage, healing, effect attachments — each its own event. One round passes.

allowed_modes class-attribute

allowed_modes: frozenset[SessionMode] = _FIELD_MODES

command_type class-attribute instance-attribute

command_type: Literal['cast_spell'] = 'cast_spell'

character_id instance-attribute

character_id: str

spell_id instance-attribute

spell_id: str

mode instance-attribute

mode: str

reversed class-attribute instance-attribute

reversed: bool = False

targets class-attribute instance-attribute

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

CloseDoor

Bases: Command

Close an open door on one side of the party's cell (zero time).

The party must be exploring a dungeon (see EnterDungeon).

Modes

exploring

Rejections
  • session.command.wrong_mode — the session is not exploring a dungeon.
  • exploration.door.no_door — no known door on that side of the cell.
  • exploration.door.already_closed — the door is already closed.
  • exploration.door.wedged — a wedged door cannot swing.
Events

DoorEvent with code exploration.door.closed.

allowed_modes class-attribute

command_type class-attribute instance-attribute

command_type: Literal['close_door'] = 'close_door'

direction instance-attribute

direction: Direction

Command

Bases: BaseModel

Base class for all commands.

Commands are frozen: they are requests, logged verbatim when accepted, never mutated. Subclasses must keep extra="ignore" (the additive-schema contract) and declare a single-valued command_type Literal plus their legal session modes.

command_type instance-attribute

command_type: str

allowed_modes class-attribute

allowed_modes: frozenset[SessionMode] = _ALL_MODES

CommandResult

Bases: BaseModel

The execute envelope: accepted or rejected, with the events either way.

A rejected command consumes no RNG draws, no clock time, mutates nothing, and is excluded from the command log — its result carries the rejections and no events.

accepted instance-attribute

accepted: bool

rejections class-attribute instance-attribute

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

events class-attribute instance-attribute

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

DropItems

Bases: Command

Drop items and coins onto the party's cell (or the pursuit trail).

Each item_ids entry drops one unit (repeat an id for more). Legal while exploring a dungeon and during an encounter — dropping treasure or food is the pursuit-distraction move.

Modes

exploring, encounter

Rejections
  • session.command.wrong_mode — the session is in town, in battle, or over.
  • session.command.unknown_membercharacter_id names no party member.
  • session.command.member_incapacitated — the member cannot act.
  • items.curse.stuck — a revealed cursed item cannot be discarded.
  • exploration.item.not_carried — the member lacks an item or the coins.
  • encounter.none_active — defensive twin of the mode gate; not reachable through normal play.
Events

ItemsDroppedEvent with what fell. In an encounter the round then closes — the monsters act per their stance — or, mid-pursuit, a PursuitEvent round resolves with the drop as bait.

allowed_modes class-attribute

command_type class-attribute instance-attribute

command_type: Literal['drop_items'] = 'drop_items'

character_id instance-attribute

character_id: str

item_ids class-attribute instance-attribute

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

coins class-attribute instance-attribute

coins: Coins = Coins()

EngageBattle

Bases: Command

Open battle: every offensive action goes through here (except turn undead).

An encounter must be open. Monsters surprised at the encounter's start grant the party a free opening round; engaging mid-pursuit turns the party to fight at the current gap.

Modes

encounter

Rejections
  • session.command.wrong_mode — no encounter is open.
  • encounter.none_active — defensive twin of the mode gate; not reachable through normal play.
Events

BattleStartedEvent; groups at morale 2 rout at once (MonsterFledEvent), and a battle whose every group routs ends immediately (BattleEndedEvent and the encounter's conclusion).

allowed_modes class-attribute

command_type class-attribute instance-attribute

command_type: Literal['engage_battle'] = 'engage_battle'

EnterDungeon

Bases: Command

Travel from town to a dungeon's entrance and start exploring.

The party must be in town. Travel takes the adventure's authored cost in turns; arrival places the party at the entrance and switches the session to exploring. Departure also snapshots the party's treasure valuation — the end-of-adventure XP award is the delta against it.

Modes

town

Rejections
  • session.command.wrong_mode — the party is not in town.
  • session.command.unknown_locationdungeon_id names no dungeon, or the dungeon has no entrance level.
Events

LocationEnteredEvent for the dungeon, after the travel time's own events. Arrival runs the entrance cell's entry checks (area treasure, room traps, keyed encounters), each reporting its own events.

allowed_modes class-attribute

command_type class-attribute instance-attribute

command_type: Literal['enter_dungeon'] = 'enter_dungeon'

dungeon_id instance-attribute

dungeon_id: str

EquipItem

Bases: Command

Equip an item from a member's item list (zero time).

Legal in town and while exploring. Class armour and weapon policies validate before anything changes. item_id is the magic item's instance id for a magic item, or the catalog id (from load_equipment — see the equipment id index) for a mundane one, which has no per-instance id.

Modes

town, exploring

Rejections
  • session.command.wrong_mode — an encounter or battle is underway, or the game is over.
  • session.command.unknown_membercharacter_id names no party member.
  • session.command.member_incapacitated — the member cannot act.
  • exploration.item.not_carried — nothing by that id in the item list.
  • items.equip.armour_forbidden, items.equip.armour_not_allowed, items.equip.shield_forbidden, items.equip.weapon_not_allowed, items.equip.weapon_forbidden — the class policy forbids it.
  • items.equip.two_handed_with_shield — a two-handed weapon and a shield cannot pair.
  • items.equip.not_equippable — potions, scrolls, ammunition, and plain gear without a combat use do not equip.
  • items.equip.not_usable — the magic device is not usable by this class.
  • items.ring.hands_full — two rings are already worn.
Events

Usually none. Equipping a worn magic item can attach its effects (EffectAttachedEvent); a cursed ring identifies and reveals at wearing (ItemIdentifiedEvent, CurseRevealedEvent).

allowed_modes class-attribute

allowed_modes: frozenset[SessionMode] = _FIELD_MODES

command_type class-attribute instance-attribute

command_type: Literal['equip_item'] = 'equip_item'

character_id instance-attribute

character_id: str

item_id instance-attribute

item_id: str

Evade

Bases: Command

Flee the encounter (legal only before battle begins, RAW).

An encounter must be open. drop scatters distraction bait as the party runs: treasure tempts intelligent monsters, food unintelligent ones. Only attacking or hostile monsters pursue; outrunning them ends the encounter cleanly.

Modes

encounter

Rejections
  • session.command.wrong_mode — no encounter is open.
  • encounter.none_active — defensive twin of the mode gate; not reachable through normal play.
  • encounter.evade.already_evading — the pursuit is already running.
  • encounter.evade.nothing_to_drop — no coins (for treasure) or rations (for food) to scatter.
Events

ItemsDroppedEvents for scattered bait, then EvasionEvent with code encounter.evasion.succeeded — the encounter ends (EncounterEndedEvent) — or encounter.evasion.pursuit, and PursuitEvent rounds follow: escape, exhaustion at the round cap, or battle at the party's heels.

allowed_modes class-attribute

command_type class-attribute instance-attribute

command_type: Literal['evade'] = 'evade'

drop class-attribute instance-attribute

drop: Literal['none', 'treasure', 'food'] = 'none'

ExtinguishSource

Bases: Command

Extinguish the bearer's burning source, forfeiting the remainder (zero time).

Legal in town and while exploring. A doused torch or lantern is spent — the remaining burn time does not bank.

Modes

town, exploring

Rejections
  • session.command.wrong_mode — an encounter or battle is underway, or the game is over.
  • session.command.unknown_membercharacter_id names no party member.
  • session.command.member_incapacitated — the member cannot act.
  • exploration.light.not_burning — the member carries no burning torch or lantern.
Events

An EffectReleasedEvent and a LightEvent with code exploration.light.extinguished per doused source.

allowed_modes class-attribute

allowed_modes: frozenset[SessionMode] = _FIELD_MODES

command_type class-attribute instance-attribute

command_type: Literal['extinguish_source'] = 'extinguish_source'

character_id instance-attribute

character_id: str

ForceDoor

Bases: Command

Force a stuck door: the character's STR open-doors check; noise is the cost.

The party must be exploring a dungeon (see EnterDungeon). Any attempt bangs on the door — the next wandering check takes the noise bonus — and a failed attempt alerts the room beyond, denying the party surprise there.

Modes

exploring

Rejections
  • session.command.wrong_mode — the session is not exploring a dungeon.
  • session.command.unknown_membercharacter_id names no party member.
  • session.command.member_incapacitated — the member cannot act.
  • exploration.door.no_door — no known door on that side of the cell.
  • exploration.door.already_open — the door already stands open.
  • exploration.door.locked — locked doors need PickLock, not muscle.
  • exploration.door.not_stuck — an unstuck door opens with OpenDoor.
Events

DoorEvent with code exploration.door.forced on success or exploration.door.stuck on failure.

allowed_modes class-attribute

command_type class-attribute instance-attribute

command_type: Literal['force_door'] = 'force_door'

direction instance-attribute

direction: Direction

character_id instance-attribute

character_id: str

GrantCoins

Bases: Command

Referee: place coins directly into a member's purse.

Referee commands are legal in every mode.

Modes

town, exploring, encounter, battle, game_over

Rejections
  • session.command.unknown_membercharacter_id names no party member.
Events

ItemAcquiredEvent with the coin value.

command_type class-attribute instance-attribute

command_type: Literal['grant_coins'] = 'grant_coins'

character_id instance-attribute

character_id: str

coins instance-attribute

coins: Coins

GrantItem

Bases: Command

Referee: place an item directly into a member's inventory.

Referee commands are legal in every mode and are logged and replayed like any other.

Modes

town, exploring, encounter, battle, game_over

Rejections
  • session.command.unknown_membercharacter_id names no party member.
  • session.command.unknown_itemitem_id names no equipment item.
Events

ItemAcquiredEvent with the granted items.

command_type class-attribute instance-attribute

command_type: Literal['grant_item'] = 'grant_item'

character_id instance-attribute

character_id: str

item_id instance-attribute

item_id: str

quantity class-attribute instance-attribute

quantity: int = Field(default=1, ge=1)

IdentifyItem

Bases: Command

Referee: identify a magic item outright — game-driven identification.

Referee commands are legal in every mode and are logged and replayed like any other.

Modes

town, exploring, encounter, battle, game_over

Rejections
  • session.command.unknown_membercharacter_id names no party member.
  • session.command.unknown_item — the member carries no magic item with that instance id.
Events

ItemIdentifiedEvent; a cursed item also reveals with a CurseRevealedEvent.

command_type class-attribute instance-attribute

command_type: Literal['identify_item'] = 'identify_item'

character_id instance-attribute

character_id: str

item_id instance-attribute

item_id: str

InspectTreasure

Bases: Command

Search a treasure feature for a treasure trap: thief-only, one turn.

The party must be exploring a dungeon (see EnterDungeon), with light. One attempt per character per feature.

Modes

exploring

Rejections
  • session.command.wrong_mode — the session is not exploring a dungeon.
  • session.command.unknown_membercharacter_id names no party member.
  • session.command.member_incapacitated — the member cannot act.
  • exploration.trap.not_a_thief — the member has no thief skills.
  • exploration.feature.unknownfeature_id names no treasure cache on this cell.
  • exploration.action.requires_light — inspecting needs real light.
  • exploration.search.already_tried — this character already inspected this feature.
Events

DetectionRolledEvent with the skill roll, then a TrapEvent with code exploration.trap.found or a SearchCompletedEvent reporting nothing. One turn passes with its usual follow-on events.

allowed_modes class-attribute

command_type class-attribute instance-attribute

command_type: Literal['inspect_treasure'] = 'inspect_treasure'

character_id instance-attribute

character_id: str

feature_id instance-attribute

feature_id: str

LightSource

Bases: Command

Light a torch or lantern, or ignite dropped oil (one round).

Legal in town and while exploring. Without an open flame already burning in the party, the bearer needs a tinder box, and striking it is a 2-in-6 chance — the round is spent per attempt (RAW). Lighting an oil_flask ignites a flask previously dropped on the party's cell as a burning pool.

Modes

town, exploring

Rejections
  • session.command.wrong_mode — an encounter or battle is underway, or the game is over.
  • session.command.unknown_membercharacter_id names no party member.
  • session.command.member_incapacitated — the member cannot act.
  • exploration.light.not_a_sourceitem_id is not torch, lantern, or oil_flask.
  • exploration.item.not_carried — the member lacks the source (or oil for the lantern), or no dropped flask lies on the cell.
  • exploration.light.no_flame — no open flame and no tinder box.
Events

LightEvent with code exploration.light.lit — an EffectAttachedEvent carries the burn-down effect — or exploration.light.failed when the tinder does not catch. One round passes.

allowed_modes class-attribute

allowed_modes: frozenset[SessionMode] = _FIELD_MODES

command_type class-attribute instance-attribute

command_type: Literal['light_source'] = 'light_source'

character_id instance-attribute

character_id: str

item_id instance-attribute

item_id: str

ListenAtDoor

Bases: Command

Listen at a door: once per character per door, ever (zero time).

The party must be exploring a dungeon (see EnterDungeon), and the listener needs light (infravision suffices). Hearing occupants marks the party aware for the room's eventual encounter.

Modes

exploring

Rejections
  • session.command.wrong_mode — the session is not exploring a dungeon.
  • session.command.unknown_membercharacter_id names no party member.
  • session.command.member_incapacitated — the member cannot act.
  • exploration.door.no_door — no known door on that side of the cell.
  • exploration.action.requires_light — the party is in the dark and the listener lacks infravision.
  • exploration.listen.already_tried — this character has already listened at this door.
Events

DetectionRolledEvent with the roll, then ListenedEvent with code exploration.listen.heard or exploration.listen.silent.

allowed_modes class-attribute

command_type class-attribute instance-attribute

command_type: Literal['listen_at_door'] = 'listen_at_door'

direction instance-attribute

direction: Direction

character_id instance-attribute

character_id: str

MoveParty

Bases: Command

Move the party one cell; facing follows the movement direction.

The party must already be inside a dungeon: a fresh session starts in town, and EnterDungeon is what places the party at the entrance and switches the session to exploring.

Modes

exploring

Rejections
  • session.command.wrong_mode — the session is not exploring a dungeon.
  • exploration.move.cannot_move — the party cannot move: it is overloaded, or a living member is unable to walk.
  • exploration.move.blocked — a wall, a closed or secret door, or the map edge blocks that direction.
Events

PartyMovedEvent with the new position and facing. Entering a new cell can also trigger area descriptions, keyed encounters, traps, treasure discovery, wandering-monster checks, light burn-down, and doors swinging shut, each reported by its own event.

allowed_modes class-attribute

command_type class-attribute instance-attribute

command_type: Literal['move_party'] = 'move_party'

direction instance-attribute

direction: Direction

OpenDoor

Bases: Command

Open an unstuck, unlocked door on one side of the party's cell (zero time).

The party must be exploring a dungeon (see EnterDungeon). An undiscovered secret door rejects exactly like blank wall — commands never leak hidden geometry.

Modes

exploring

Rejections
  • session.command.wrong_mode — the session is not exploring a dungeon.
  • exploration.door.no_door — no known door on that side of the cell (undiscovered secret doors included).
  • exploration.door.already_open — the door already stands open.
  • exploration.door.locked — the lock has not been picked or otherwise undone.
  • exploration.door.stuck — a stuck door needs ForceDoor.
Events

DoorEvent with code exploration.door.opened.

allowed_modes class-attribute

command_type class-attribute instance-attribute

command_type: Literal['open_door'] = 'open_door'

direction instance-attribute

direction: Direction

Parley

Bases: Command

Speak with the monsters: a fresh reaction roll with the speaker's CHA modifier.

An encounter must be open — encounters begin from wandering checks, keyed areas, or the referee spawn commands. Any number of re-rolls is legal; a hostile turn self-limits the conversation.

Modes

encounter

Rejections
  • session.command.wrong_mode — no encounter is open.
  • encounter.none_active — defensive twin of the mode gate; not reachable through normal play.
  • encounter.parley.mid_pursuit — no talking while being chased.
  • session.command.unknown_membercharacter_id names no party member.
  • session.command.member_incapacitated — the speaker cannot act.
Events

ReactionRolledEvent, and a StanceChangedEvent when the stance shifts. An attacks result opens battle at once (BattleStartedEvent and what follows); otherwise the encounter round closes with the monsters' beat.

allowed_modes class-attribute

command_type class-attribute instance-attribute

command_type: Literal['parley'] = 'parley'

character_id instance-attribute

character_id: str

PickLock

Bases: Command

Pick a locked door's lock: thief-only, needs thieves' tools, one turn.

The party must be exploring a dungeon (see EnterDungeon). A failed attempt locks that character out of that lock until the next level gain.

Modes

exploring

Rejections
  • session.command.wrong_mode — the session is not exploring a dungeon.
  • session.command.unknown_membercharacter_id names no party member.
  • session.command.member_incapacitated — the member cannot act.
  • exploration.lock.not_a_thief — the member has no thief skills.
  • exploration.lock.no_tools — the member carries no thieves' tools.
  • exploration.door.no_door — no known door on that side of the cell.
  • exploration.lock.not_locked — the door has no lock left to pick.
  • exploration.action.requires_light — picking needs real light; infravision does not suffice.
  • exploration.lock.locked_out — this character already failed here at their current level.
Events

DetectionRolledEvent with the skill roll and, on success, a DoorEvent with code exploration.door.unlocked. The attempt costs one turn, whose bookkeeping (light burn-down, the rest cadence, wandering checks) reports through its own events.

allowed_modes class-attribute

command_type class-attribute instance-attribute

command_type: Literal['pick_lock'] = 'pick_lock'

direction instance-attribute

direction: Direction

character_id instance-attribute

character_id: str

PlaceParty

Bases: Command

Referee: teleport the party to a location.

Referee commands are legal in every mode, except that the party cannot be teleported out of an open encounter or battle. Placing into a dungeon marks the cell explored and switches the session to exploring; placing in town switches it to town.

Modes

town, exploring, encounter, battle, game_over

Rejections
  • session.command.encounter_in_progress — an encounter or battle is open.
  • session.command.unknown_location — the location names no dungeon level.
  • session.command.out_of_bounds — the position is off the level's grid.
Events

LocationEnteredEvent for the destination.

command_type class-attribute instance-attribute

command_type: Literal['place_party'] = 'place_party'

location instance-attribute

location: PartyLocation

PrepareSpells

Bases: Command

Prepare a caster's daily spells: once per sleep, after an uninterrupted night, six turns.

Legal in town and while exploring. The caster must have slept (a night or day Rest) since the last preparation; the selections replace the memorized list wholesale.

Modes

town, exploring

Rejections
  • session.command.wrong_mode — an encounter or battle is underway, or the game is over.
  • session.command.unknown_membercharacter_id names no party member.
  • session.command.member_incapacitated — the member cannot act.
  • magic.memorize.needs_sleep — no sleep since the last preparation.
  • magic.memorize.not_a_caster — the class casts no spells.
  • magic.memorize.unknown_spell — a selection names no known spell.
  • magic.memorize.wrong_list — a selection is off the caster's spell list.
  • magic.memorize.divine_reverses_at_cast — divine casters choose the reversed form at casting, not at prayer.
  • magic.memorize.not_in_book — an arcane selection is missing from the spell book.
  • magic.memorize.not_reversible — a reversed selection has no reversed form.
  • magic.memorize.slots_exceeded — more selections at some spell level than the caster has slots.
Events

SpellsMemorizedEvent with the prepared list. Six turns pass with their usual follow-on events.

allowed_modes class-attribute

allowed_modes: frozenset[SessionMode] = _FIELD_MODES

command_type class-attribute instance-attribute

command_type: Literal['prepare_spells'] = 'prepare_spells'

character_id instance-attribute

character_id: str

selections class-attribute instance-attribute

selections: tuple[MemorizedSpell, ...] = ()

PurchaseEquipment

Bases: Command

Buy equipment in town: each item_ids entry buys one purchase lot (zero time).

The party must be in town. The whole basket prices first; if the member cannot afford the total, nothing is bought.

Modes

town

Rejections
  • session.command.wrong_mode — the party is not in town.
  • session.command.unknown_membercharacter_id names no party member.
  • session.command.member_incapacitated — the member cannot act.
  • session.command.unknown_item — an entry names no equipment item.
  • items.purchase.insufficient_funds — the purse cannot cover the total.
Events

ItemAcquiredEvent listing the purchases.

allowed_modes class-attribute

command_type class-attribute instance-attribute

command_type: Literal['purchase_equipment'] = 'purchase_equipment'

character_id instance-attribute

character_id: str

item_ids class-attribute instance-attribute

item_ids: tuple[str, ...] = Field(min_length=1)

PurchaseHealing

Bases: Command

Buy a temple healing service in town (zero time).

The party must be in town. The service list and prices are a documented adaptation — the OSE SRD's base-town material is prose: cure light wounds 25 gp, cure serious wounds 100 gp, cure disease 150 gp, neutralize poison 150 gp, remove curse 200 gp, raise dead 1,500 gp. Each resolves through the kernel spell path with an abstract temple cleric at the minimum level able to cast the spell; the named character is the target and pays from their own purse.

Modes

town

Rejections
  • session.command.wrong_mode — the party is not in town.
  • session.command.unknown_membercharacter_id names no party member.
  • items.purchase.insufficient_funds — the character's purse cannot cover the service.
Events

HealingPurchasedEvent, then the service spell's own resolution events (healing, effect releases, a revival's outcome).

allowed_modes class-attribute

command_type class-attribute instance-attribute

command_type: Literal['purchase_healing'] = 'purchase_healing'

character_id instance-attribute

character_id: str

service instance-attribute

service: Literal[
    "cure_light_wounds", "cure_serious_wounds", "cure_disease", "neutralize_poison", "remove_curse", "raise_dead"
]

RemoveTreasureTrap

Bases: Command

Remove a found treasure trap: thief-only, one turn; failure springs it.

The party must be exploring a dungeon (see EnterDungeon), with light, and the trap must already have been found by InspectTreasure.

Modes

exploring

Rejections
  • session.command.wrong_mode — the session is not exploring a dungeon.
  • session.command.unknown_membercharacter_id names no party member.
  • session.command.member_incapacitated — the member cannot act.
  • exploration.trap.not_a_thief — the member has no thief skills.
  • exploration.feature.unknownfeature_id names no trapped feature on this cell.
  • exploration.trap.not_found — the trap has not been found yet.
  • exploration.trap.already_resolved — the trap was already removed or has already sprung.
  • exploration.action.requires_light — removal needs real light.
  • exploration.search.already_tried — this character already attempted the removal.
Events

DetectionRolledEvent with the skill roll, then a TrapEvent: exploration.trap.removed on success, exploration.trap.sprung on failure — the sprung trap resolves at once against the thief (saving throws, damage, conditions, each its own event). One turn passes.

allowed_modes class-attribute

command_type class-attribute instance-attribute

command_type: Literal['remove_treasure_trap'] = 'remove_treasure_trap'

character_id instance-attribute

character_id: str

feature_id instance-attribute

feature_id: str

ReorderParty

Bases: Command

Rewrite the marching order — the only way marching order changes.

Legal in town and while exploring; the order is locked once an encounter or battle has begun.

Modes

town, exploring

Rejections
  • session.command.wrong_mode — an encounter or battle is underway, or the game is over.
  • exploration.party.bad_orderorder does not name exactly the current members, each once.
Events

None. An accepted reorder changes state silently.

allowed_modes class-attribute

allowed_modes: frozenset[SessionMode] = _FIELD_MODES

command_type class-attribute instance-attribute

command_type: Literal['reorder_party'] = 'reorder_party'

order class-attribute instance-attribute

order: tuple[str, ...] = Field(min_length=1)

ResolveBattleRound

Bases: Command

Resolve one battle round: one declaration per living, able party member.

A battle must be underway (see EngageBattle). Validation is the pure pre-phase: every declaration validates or the whole command rejects listing every rejection — partial acceptance would tangle the replay contract.

Modes

battle

Rejections
  • session.command.wrong_mode — no battle is underway.
  • battle.none_active — defensive twin of the mode gate; not reachable through normal play.
  • battle.declaration.roster_mismatch — the declarations do not name exactly the living, able members.
  • battle.declaration.unknown_action — an unrecognized action.
  • Move declarations: battle.declaration.missing_move, battle.declaration.unknown_group, battle.declaration.cannot_move.
  • Attack declarations: battle.declaration.unknown_group, battle.declaration.no_target, battle.declaration.weapon_not_wielded, battle.declaration.not_in_front_rank, and the kernel attack checks — combat.attack.out_of_reach, combat.attack.out_of_range, combat.attack.reload, combat.attack.attacker_incapacitated, combat.attack.attacker_blind.
  • Cast declarations: battle.declaration.missing_spell, battle.declaration.unknown_group, battle.declaration.invisible_target, and the cast checks — magic.cast.unknown_spell, magic.cast.silenced_area, magic.cast.unknown_mode, magic.cast.unknown_target, magic.cast.not_memorized, magic.cast.caster_incapacitated, magic.cast.caster_restrained, magic.cast.anti_magic_shell, magic.cast.not_reversible, magic.cast.target_count, magic.cast.out_of_range.
  • Turn-undead declarations: magic.turning.not_a_turner, magic.turning.caster_incapacitated.
  • Item declarations: battle.declaration.item_unusable, battle.declaration.unknown_group, battle.declaration.no_target, items.use.not_usable, items.device.inert, items.scroll.spent, items.scroll.no_such_spell, items.scroll.wrong_caster, exploration.action.requires_light, combat.attack.out_of_reach.
Events

BattleRoundEvent opens the round; declared casts post as SpellDeclaredEvents; InitiativeRolledEvent orders the sides. The phases then report themselves — movement, missiles, magic, melee: attack and damage rolls, saving throws, casts and disruptions, morale checks, routs and defeats — each its own event. A terminal round appends BattleEndedEvent and the encounter's conclusion, or GameOverEvent on a party wipe.

allowed_modes class-attribute

command_type class-attribute instance-attribute

command_type: Literal['resolve_battle_round'] = 'resolve_battle_round'

declarations class-attribute instance-attribute

declarations: tuple[BattleDeclaration, ...] = ()

Rest

Bases: Command

Rest: one turn (the cadence rest), a night (48 turns), or a full day (144).

Legal in town and while exploring. In the dungeon a wandering encounter can interrupt the rest; a full day of rest also applies natural healing.

Modes

town, exploring

Rejections
  • session.command.wrong_mode — an encounter or battle is underway, or the game is over.
Events

RestedEvent with code exploration.rest.rested, or exploration.rest.interrupted when a wandering encounter breaks the rest. Clearing fatigue or exhaustion reports a FatigueEvent or ExhaustionEvent; a full day's natural healing an HealingAppliedEvent. The elapsed turns report their own bookkeeping (light burn-down, provisions, wandering checks).

allowed_modes class-attribute

allowed_modes: frozenset[SessionMode] = _FIELD_MODES

command_type class-attribute instance-attribute

command_type: Literal['rest'] = 'rest'

kind instance-attribute

kind: Literal['turn', 'night', 'day']

RollDice

Bases: Command

Referee: roll a dice expression through the seeded session.

An authorial roll for freeform adjudication — the referee resolves a chance outcome the content model can't express (a puzzle, a bluff, "does the frayed rope hold?") by rolling through the engine rather than inventing a number, so the result is logged, replayable, and grounded in a typed event. Referee commands are legal in every mode. The roll draws from the dedicated ADJUDICATION_STREAM, so an ad-hoc referee roll never perturbs the draw sequence of keyed mechanics. A malformed expression is rejected at construction, exactly as SpawnMonsters's count_dice is, so it never reaches the session and consumes no draw.

Modes

town, exploring, encounter, battle, game_over

Rejections

None.

Events

DiceRolledEvent with the expression, the total, and the individual die results.

command_type class-attribute instance-attribute

command_type: Literal['roll_dice'] = 'roll_dice'

expression instance-attribute

expression: str

Search

Bases: Command

Search the party's cell for one hidden-feature kind (one turn).

The party must be exploring a dungeon (see EnterDungeon), with light (infravision suffices). Each character gets one attempt per cell per kind, ever.

Modes

exploring

Rejections
  • session.command.wrong_mode — the session is not exploring a dungeon.
  • session.command.unknown_membercharacter_id names no party member.
  • session.command.member_incapacitated — the member cannot act.
  • exploration.action.requires_light — the party is in the dark and the searcher lacks infravision.
  • exploration.search.already_tried — this character already searched this cell for this kind.
Events

DetectionRolledEvent with the roll, a TrapEvent when a room trap is found, then SearchCompletedEvent naming what turned up. One turn passes with its usual follow-on events.

allowed_modes class-attribute

command_type class-attribute instance-attribute

command_type: Literal['search'] = 'search'

character_id instance-attribute

character_id: str

kind instance-attribute

kind: Literal['secret_doors', 'room_traps', 'construction']

SessionMode

Bases: StrEnum

The session modes gating command legality.

The wire values are lowercase — they serialize into saves; changing them is a schema_version bump.

TOWN class-attribute instance-attribute

TOWN = 'town'

EXPLORING class-attribute instance-attribute

EXPLORING = 'exploring'

ENCOUNTER class-attribute instance-attribute

ENCOUNTER = 'encounter'

BATTLE class-attribute instance-attribute

BATTLE = 'battle'

GAME_OVER class-attribute instance-attribute

GAME_OVER = 'game_over'

SetDoorState

Bases: Command

Referee: rewrite a door's overlay anywhere (None fields stay unchanged).

Referee commands are legal in every mode; the door may be on any level of any dungeon, not just under the party.

Modes

town, exploring, encounter, battle, game_over

Rejections
  • session.command.unknown_locationdungeon_id or level_number resolves to nothing.
  • session.command.no_door — no door edge at that cell and direction.
Events

A referee-visibility DoorEvent when the open state actually changes; otherwise none.

command_type class-attribute instance-attribute

command_type: Literal['set_door_state'] = 'set_door_state'

dungeon_id instance-attribute

dungeon_id: str

level_number class-attribute instance-attribute

level_number: int = Field(ge=1)

x instance-attribute

x: int

y instance-attribute

y: int

direction instance-attribute

direction: Direction

open class-attribute instance-attribute

open: bool | None = None

wedged class-attribute instance-attribute

wedged: bool | None = None

discovered class-attribute instance-attribute

discovered: bool | None = None

unlocked class-attribute instance-attribute

unlocked: bool | None = None

SellTreasure

Bases: Command

Sell valuables in town at full value (zero time).

The party must be in town. Each entry names a carried valuable's instance id; the coins credit its carrier's purse. osrlib adopts full value_gp as the sale price: the OSE SRD prices treasure but names no exchange spread, and full value keeps the 1-gp-1-XP identity clean. Magic items have no fixed sale value (RAW's own words) and reject; revealed curses stick.

Modes

town

Rejections
  • session.command.wrong_mode — the party is not in town.
  • town.sell.no_fixed_value — magic items cannot be sold for a fixed price.
  • exploration.item.not_carried — no member carries a valuable with that instance id.
Events

TreasureSoldEvent per selling member, with the credited value.

allowed_modes class-attribute

command_type class-attribute instance-attribute

command_type: Literal['sell_treasure'] = 'sell_treasure'

item_ids class-attribute instance-attribute

item_ids: tuple[str, ...] = Field(min_length=1)

SetFlag

Bases: Command

Referee: set a session flag (content wiring: the lever opens the portcullis).

Referee commands are legal in every mode. Flags serialize into saves; game code and listeners read them back.

Modes

town, exploring, encounter, battle, game_over

Rejections

None.

Events

FlagSetEvent with the key and value.

command_type class-attribute instance-attribute

command_type: Literal['set_flag'] = 'set_flag'

key class-attribute instance-attribute

key: str = Field(min_length=1)

value instance-attribute

value: str | int | bool

SpawnMonsters

Bases: Command

Referee: spawn monsters and open an encounter at a distance.

Referee commands are legal in every mode, but the party must be standing in a dungeon with no encounter already open — encounters live on the dungeon grid. Exactly one of count_dice or count_fixed is required.

Modes

town, exploring, encounter, battle, game_over

Rejections
  • session.command.unknown_monstertemplate_id names no monster.
  • session.command.encounter_in_progress — an encounter or battle is already open.
  • session.command.not_in_dungeon — the party is not on a dungeon cell.
Events

MonstersSpawnedEvent, then the encounter opening — SurpriseRolledEvents, EncounterStartedEvent, the reaction roll and StanceChangedEvent; an attacks stance opens battle at once.

command_type class-attribute instance-attribute

command_type: Literal['spawn_monsters'] = 'spawn_monsters'

template_id instance-attribute

template_id: str

count_dice class-attribute instance-attribute

count_dice: str | None = None

count_fixed class-attribute instance-attribute

count_fixed: int | None = Field(default=None, ge=1)

distance_feet class-attribute instance-attribute

distance_feet: int = Field(ge=0)

SpawnNpcParty

Bases: Command

Referee: generate an NPC adventuring party and open an encounter.

count_dice=None rolls the compiled composition dice (Basic 1d4+4, Expert 1d6+3) — the surface for keyed content, quest listeners, and tests. Referee commands are legal in every mode, but the party must be standing in a dungeon with no encounter already open.

Modes

town, exploring, encounter, battle, game_over

Rejections
  • session.command.encounter_in_progress — an encounter or battle is already open.
  • session.command.not_in_dungeon — the party is not on a dungeon cell.
Events

NpcPartySpawnedEvent (the referee-visibility roster), then the encounter opening as with SpawnMonsters.

command_type class-attribute instance-attribute

command_type: Literal['spawn_npc_party'] = 'spawn_npc_party'

party_kind instance-attribute

party_kind: Literal['basic', 'expert']

count_dice class-attribute instance-attribute

count_dice: str | None = None

distance_feet class-attribute instance-attribute

distance_feet: int = Field(ge=0)

TakeTreasure

Bases: Command

Empty a cache or pile into the party's packs (one turn, RAW).

The party must be exploring a dungeon (see EnterDungeon). feature_id names an authored cache, an engine-generated cache, or the literal pile for goods dropped on the cell. The leading living member carries everything; taking a trapped cache with its trap unresolved risks springing it.

Modes

exploring

Rejections
  • session.command.wrong_mode — the session is not exploring a dungeon.
  • session.command.no_living_members — no one is left to carry.
  • exploration.feature.unknown — nothing by that id on this cell.
  • exploration.feature.emptied — the cache has already been emptied.
Events

ItemAcquiredEvent listing the goods and coin value. An unresolved treasure trap rolls first (DetectionRolledEvent, a TrapEvent, and the trap's resolution when it springs). Under the immediate XP timing an XpAwardedEvent follows per member. One turn passes.

allowed_modes class-attribute

command_type class-attribute instance-attribute

command_type: Literal['take_treasure'] = 'take_treasure'

feature_id instance-attribute

feature_id: str

TravelToTown

Bases: Command

Travel from the dungeon entrance back to town (the same travel cost).

The party must be exploring and standing on the entrance cell. Doors the party opened swing shut behind it, and under the on-return XP timing the adventure award pays out on arrival.

Modes

exploring

Rejections
  • session.command.wrong_mode — the session is not exploring a dungeon.
  • exploration.travel.not_at_entrance — the party is not on the entrance cell.
Events

DoorEvents for doors swinging shut, travel-time bookkeeping, then LocationEnteredEvent for town. Under the on-return XP timing an AdventureXpAwardEvent and per-member XpAwardedEvents follow.

allowed_modes class-attribute

command_type class-attribute instance-attribute

command_type: Literal['travel_to_town'] = 'travel_to_town'

TurnParty

Bases: Command

Turn the party in place to a new facing (zero time).

The party must already be inside a dungeon — see EnterDungeon.

Modes

exploring

Rejections
  • session.command.wrong_mode — the session is not exploring a dungeon.
Events

PartyMovedEvent with the unchanged position and the new facing.

allowed_modes class-attribute

command_type class-attribute instance-attribute

command_type: Literal['turn_party'] = 'turn_party'

facing instance-attribute

facing: Direction

TurnUndead

Bases: Command

Present the holy symbol — the one aggressive act with a pre-battle procedure.

An encounter must be open: exploration offers no candidates by definition, and in battle turning is a declaration kind. If any monster stands unturned, the survivors attack at once.

Modes

encounter

Rejections
  • session.command.wrong_mode — no encounter is open.
  • encounter.none_active — defensive twin of the mode gate; not reachable through normal play.
  • encounter.turning.mid_pursuit — no turning while being chased.
  • session.command.unknown_membercharacter_id names no party member.
  • session.command.member_incapacitated — the cleric cannot act.
  • magic.turning.not_a_turner — the class has no turn-undead ability.
  • magic.turning.caster_incapacitated — a condition prevents the attempt.
Events

UndeadTurnedEvent with the roll and the affected monsters (their conditions each their own event). When every monster is turned or destroyed the encounter ends (EncounterEndedEvent); otherwise a StanceChangedEvent to attacks and battle opens (BattleStartedEvent).

allowed_modes class-attribute

command_type class-attribute instance-attribute

command_type: Literal['turn_undead'] = 'turn_undead'

character_id instance-attribute

character_id: str

UnequipItem

Bases: Command

Return an equipped item to the member's item list (zero time).

Legal in town and while exploring. A revealed cursed item stays put until remove curse.

Modes

town, exploring

Rejections
  • session.command.wrong_mode — an encounter or battle is underway, or the game is over.
  • session.command.unknown_membercharacter_id names no party member.
  • session.command.member_incapacitated — the member cannot act.
  • exploration.item.not_equipped — nothing by that id is equipped.
  • items.curse.stuck — a revealed cursed item cannot be removed.
Events

Usually none; a worn magic item's effects release (EffectReleasedEvent).

allowed_modes class-attribute

allowed_modes: frozenset[SessionMode] = _FIELD_MODES

command_type class-attribute instance-attribute

command_type: Literal['unequip_item'] = 'unequip_item'

character_id instance-attribute

character_id: str

item_id instance-attribute

item_id: str

UseItem

Bases: Command

Use a magic item: drink a potion, read a scroll, activate a device (one round).

One round is the RAW activation cost (drinking is one round). target_id names a character (the staff of healing's touch) or an encounter group (a device's area); spell_id, mode, and targets select the inscribed spell and its targets when reading a multi-spell scroll (the CastSpell surface). In battle, item use is the use_item declaration instead. First meaningful use identifies the item — and reveals its curse.

Modes

exploring, encounter

Rejections
  • session.command.wrong_mode — the session is in town, in battle, or over.
  • session.command.unknown_membercharacter_id names no party member.
  • session.command.member_incapacitated — the member cannot act.
  • exploration.item.not_carried — the member carries no magic item with that instance id.
  • items.use.not_usable — the item has no usable action, or the class cannot use the device.
  • Scrolls: exploration.action.requires_light (reading needs real light), items.scroll.spent, items.scroll.no_such_spell, items.scroll.wrong_caster, and the cast validation codes (magic.cast.unknown_target, magic.cast.unknown_mode, magic.cast.target_count, magic.cast.out_of_range, magic.cast.caster_incapacitated, magic.cast.caster_restrained, magic.cast.anti_magic_shell).
  • Devices: items.device.inert (no charges left), items.use.target_required, items.use.unknown_target, and items.use.battle_only (a striking effect is a battle declaration).
Events

ItemUsedEvent naming what happened (drunk, read, activated — or mixed potions, or a cursed scroll), with ItemIdentifiedEvent and CurseRevealedEvent at first meaningful use, then the item's own resolution — healing, saving throws, damage, effect attachments, a scroll's SpellCastEvent — each its own event. One round passes (in an encounter, the round beat follows instead).

allowed_modes class-attribute

command_type class-attribute instance-attribute

command_type: Literal['use_item'] = 'use_item'

character_id instance-attribute

character_id: str

item_id instance-attribute

item_id: str

target_id class-attribute instance-attribute

target_id: str | None = None

spell_id class-attribute instance-attribute

spell_id: str | None = None

mode class-attribute instance-attribute

mode: str | None = None

targets class-attribute instance-attribute

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

UseStairs

Bases: Command

Take the stair, ladder, or other transition on the party's cell.

The party must be exploring a dungeon (see EnterDungeon) and standing on a cell with an authored transition. The move costs one unexplored-cell step of movement.

Modes

exploring

Rejections
  • session.command.wrong_mode — the session is not exploring a dungeon.
  • exploration.stairs.none — no transition on the party's cell.
Events

LocationEnteredEvent when the level or dungeon changes. Arrival then runs the cell's entry checks — area treasure, room traps, keyed encounters — each reporting its own events, and the movement cost accrues toward the turn clock.

allowed_modes class-attribute

command_type class-attribute instance-attribute

command_type: Literal['use_stairs'] = 'use_stairs'

Wait

Bases: Command

Hold for one encounter round; the monsters act per their stance.

An encounter must be open. Waiting burns a round to see what the monsters do — an uncertain stance re-rolls its reaction, a hostile one runs out its patience.

Modes

encounter

Rejections
  • session.command.wrong_mode — no encounter is open.
  • encounter.none_active — defensive twin of the mode gate; not reachable through normal play.
Events

The round beat's events: an uncertain stance re-rolls (ReactionRolledEvent, possibly a StanceChangedEvent), and an attacking or expired-patience hostile stance opens battle (BattleStartedEvent). During a pursuit a PursuitEvent round resolves instead.

allowed_modes class-attribute

command_type class-attribute instance-attribute

command_type: Literal['wait'] = 'wait'

WedgeDoor

Bases: Command

Wedge a door with an iron spike so it cannot swing shut (zero time).

The party must be exploring a dungeon (see EnterDungeon). Any living member's spike serves; one iron spike is consumed.

Modes

exploring

Rejections
  • session.command.wrong_mode — the session is not exploring a dungeon.
  • exploration.door.no_door — no known door on that side of the cell.
  • exploration.door.wedged — the door is already wedged.
  • exploration.door.no_spike — no living member carries iron spikes.
Events

DoorEvent with code exploration.door.wedged.

allowed_modes class-attribute

command_type class-attribute instance-attribute

command_type: Literal['wedge_door'] = 'wedge_door'

direction instance-attribute

direction: Direction

parse_command

parse_command(data: Mapping[str, object]) -> Command | None

Parse one serialized command, skipping unknown command types.

Parameters:

Name Type Description Default
data Mapping[str, object]

A mapping previously produced by a command's model_dump.

required

Returns:

Type Description
Command | None

The command, or None when its command_type is unknown.

Raises:

Type Description
ContentValidationError

If the command type is known but the payload is malformed.