Skip to content

osrlib.crawl.session

GameSession: the command loop, the event log, listeners, flags, and views.

The session is the front end's one object. Build it with GameSession.new (or restore one with load_game), feed player intent to GameSession.execute as typed commands from osrlib.crawl.commands, and render the Events that come back. The session owns what the kernel leaves to its caller: the RngStreams (master seed), the IdAllocator, the EffectsLedger, the GameClock, the entity registry (characters and live monster instances), the flag store, the listener-state store, the command and event logs, the mode, and the crawl state.

execute(command) runs a pure validation pre-phase: a rejected command consumes no RNG draws, no clock time, mutates nothing, and is excluded from the command log. Accepted commands mutate, append their events to the log, then registered listeners run in registration order, their events appended to the same result and log. Each command class documents its legal modes, rejection codes, and events.

For presentation, GameSession.view projects the state at player or referee visibility — render from views and events, never from raw session internals.

ADJUDICATION_STREAM module-attribute

ADJUDICATION_STREAM = 'adjudication'

Stream key for the referee's ad-hoc adjudication rolls — kept off the mechanical streams so a freeform roll never shifts a keyed mechanic's draw sequence.

DARKNESS_EFFECT_KINDS module-attribute

DARKNESS_EFFECT_KINDS = frozenset({'darkness', 'continual_darkness'})

The darkness-family effect kinds — the printed radii swallow a marching party.

ENCOUNTER_STREAM module-attribute

ENCOUNTER_STREAM = 'encounter'

Stream key for encounter-procedure draws: surprise, distance, reaction, distraction.

EXPLORATION_STREAM module-attribute

EXPLORATION_STREAM = 'exploration'

Stream key for exploration draws: forcing, listening, searching, traps, tinder, skills.

LIGHT_EFFECT_KINDS module-attribute

LIGHT_EFFECT_KINDS = frozenset({'light', 'continual_light'})

The light-family effect kinds: torch/lantern attachments and the light spells.

MONSTER_ACTION_STREAM module-attribute

MONSTER_ACTION_STREAM = 'monster_action'

Stream key for the action policy's draws — a policy change never shifts combat draws.

WANDERING_STREAM module-attribute

WANDERING_STREAM = 'wandering'

Stream key for wandering-monster draws: the check die, the d20, counts, variant picks.

DeathRecord

Bases: BaseModel

When and how a character died — the honest inputs for revival windows.

cause is "poison" when the killing resolution was a poison save or a poison-delay expiry (feeding neutralize poison's round window), else the source kind; raise dead's day count reads round regardless of cause.

round instance-attribute

round: int

cause instance-attribute

cause: str

DefeatedMonsterRecord

Bases: BaseModel

One defeated monster — the XP award's input.

monster_id instance-attribute

monster_id: str

template_id instance-attribute

template_id: str

outcome instance-attribute

outcome: str

xp instance-attribute

xp: int

DeprivationState

Bases: BaseModel

One member's food and water deprivation counters (worse track applies).

food_days class-attribute instance-attribute

food_days: int = 0

water_days class-attribute instance-attribute

water_days: int = 0

worst property

worst: int

The worse track — the two deprivation tracks don't stack.

GameSession

GameSession(*, party: Party, adventure: Adventure, ruleset: Ruleset, streams: RngStreams, master_seed: int)

A running game: the single entry point for command execution and views.

The loop: execute one command at a time and render its CommandResult; read state through view (player or referee visibility) rather than session attributes; extend the game with register_listener and session flags. Everything that happened is on event_log, every accepted command on command_log, and save_game/load_game round-trip the whole session deterministically: same seed, same commands, same game.

Internal constructor — use GameSession.new or load_game.

party instance-attribute

party = party

adventure instance-attribute

adventure = adventure

ruleset instance-attribute

ruleset = ruleset

streams instance-attribute

streams = streams

master_seed instance-attribute

master_seed = master_seed

allocator instance-attribute

allocator = IdAllocator()

ledger instance-attribute

ledger = EffectsLedger()

clock instance-attribute

clock = GameClock()

mode instance-attribute

dungeon_state instance-attribute

dungeon_state = DungeonState()

monsters instance-attribute

monsters: dict[str, MonsterInstance] = {}

npcs instance-attribute

npcs: dict[str, Character] = {}

flags instance-attribute

flags: dict[str, str | int | bool] = {}

listener_state instance-attribute

listener_state: dict[str, dict] = {}

listeners instance-attribute

listeners: list[Listener] = []

command_log instance-attribute

command_log: list[Command] = []

event_log instance-attribute

event_log: list[Event | dict] = []

death_records instance-attribute

death_records: dict[str, DeathRecord] = {}

defeated_monsters instance-attribute

defeated_monsters: list[DefeatedMonsterRecord] = []

deprivation instance-attribute

deprivation: dict[str, DeprivationState] = {}

treasure_snapshot_cp instance-attribute

treasure_snapshot_cp: int | None = None

odometer_thirds instance-attribute

odometer_thirds = 0

turns_since_rest instance-attribute

turns_since_rest = 0

wandering_counter instance-attribute

wandering_counter = 0

noise_since_check instance-attribute

noise_since_check = False

sleep_count instance-attribute

sleep_count = 0

last_prepared_sleep instance-attribute

last_prepared_sleep: dict[str, int] = {}

alerted_areas instance-attribute

alerted_areas: list[str] = []

heard_areas instance-attribute

heard_areas: list[str] = []

encounter instance-attribute

encounter: EncounterState | None = None

battle instance-attribute

battle: BattleState | None = None

action_policies instance-attribute

action_policies: dict[str, object] = {}

metadata property

metadata: dict[str, object]

The front-end handshake: the schema and engine versions.

new classmethod

new(party: Party, adventure: Adventure, *, seed: int, ruleset: Ruleset | None = None) -> GameSession

Create a new session, validating the adventure and assigning member ids.

Character ids assign as character-NNNN from the session's allocator in party order. Members that already carry ids keep them (a party loaded from an earlier session).

Parameters:

Name Type Description Default
party Party

The party, in marching order.

required
adventure Adventure

The frozen adventure content.

required
seed int

The master seed.

required
ruleset Ruleset | None

The ruleset in play; defaults to a stock Ruleset().

None

Returns:

Type Description
GameSession

The session, in town, at round 0.

Raises:

Type Description
ContentValidationError

If the adventure has dangling references.

execute

execute(command: Command) -> CommandResult

Execute one command: the pure validation pre-phase, then apply and log.

Parameters:

Name Type Description Default
command Command

The command to execute.

required

Returns:

Type Description
CommandResult

The result envelope; rejected commands carry rejections and no events.

Examples:

from osrlib.core.alignment import Alignment
from osrlib.core.character import CHARACTER_CREATION_STREAM, create_character
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
from osrlib.crawl.dungeon import DungeonSpec, LevelSpec
from osrlib.crawl.party import Party
from osrlib.crawl.session import GameSession

rules = Ruleset()
rng = RngStreams(master_seed=7).get(CHARACTER_CREATION_STREAM)
hero = create_character(
    name="Hild",
    class_id="fighter",
    alignment=Alignment.LAWFUL,
    ruleset=rules,
    stream=rng,
)
level = LevelSpec(number=1, width=1, height=1, entrance=(0, 0))
crypt = DungeonSpec(id="crypt", name="The Old Crypt", levels=(level,))
town = TownSpec(name="Threshold")
adventure = Adventure(name="A First Delve", town=town, dungeons=(crypt,))
session = GameSession.new(Party(members=[hero.character]), adventure, seed=7)

result = session.execute(EnterDungeon(dungeon_id="crypt"))
assert result.accepted and result.events

again = session.execute(EnterDungeon(dungeon_id="crypt"))  # already inside
assert not again.accepted
assert again.rejections[0].code == "session.command.wrong_mode"

register_listener

register_listener(listener: Listener) -> None

Register a listener; it runs after each command in registration order.

Parameters:

Name Type Description Default
listener Listener

The listener; its state snapshots into saves under its key.

required

registry

registry() -> dict[str, Any]

Live entities by id: party members (marching order), then monsters, then NPCs.

combatant

combatant(combatant_id: str) -> object | None

Return the monster or NPC with combatant_id, or None.

The encounter side's lookup: an EncounterGroup's combatant ids span monsters and NPC adventurers, and this resolves both.

Parameters:

Name Type Description Default
combatant_id str

The combatant's entity id.

required

Returns:

Type Description
object | None

The live instance, or None when the id is unknown.

member

member(character_id: str) -> Character

Return the party member with character_id (see Party.member).

spawn

spawn(template_id: str, count: int, *, alignment: Alignment | None = None) -> list[MonsterInstance]

Spawn count instances into the registry, ids from the session allocator.

Parameters:

Name Type Description Default
template_id str

A monster template id from load_monsters — see the monster id index.

required
count int

How many to spawn.

required
alignment Alignment | None

An alignment override from keyed content.

None

Returns:

Type Description
list[MonsterInstance]

The spawned instances, in spawn order.

advance_rounds

advance_rounds(n: int) -> list[Event]

Advance the clock n rounds through the ledger, translating light expiries.

A light-kind expiry is referee visibility; the session appends the player-facing exploration.light.expired with the source kind. Day-boundary crossings consume provisions.

Parameters:

Name Type Description Default
n int

How many rounds.

required

Returns:

Type Description
list[Event]

The ledger's events plus the player-facing translations.

advance_turns

advance_turns(turns: int, *, resting: bool = False, field: bool | None = None) -> tuple[list[Event], bool]

Advance whole turns one at a time, running the per-turn bookkeeping.

A mid-turn clock snaps to the next turn boundary first — turn-costing actions absorb partial round-time. Each turn: the ledger advances, day boundaries consume provisions, the rest cadence counts (unless resting), and — in the field — the wandering cadence may fire a check that starts an encounter, which stops the advance.

Parameters:

Name Type Description Default
turns int

How many turns to advance.

required
resting bool

True during a Rest (the cadence doesn't count, and the wandering chance takes the resting −1).

False
field bool | None

Whether the wandering cadence runs; defaults to "exploring in a dungeon" (town time and travel are abstract, no wandering there).

None

Returns:

Type Description
tuple[list[Event], bool]

The events, and True when a wandering encounter interrupted the span.

party_light

party_light() -> tuple[bool, bool]

Return (lit, infravision_allowed) for the party as a whole.

Lit means any living member carries an active light-family effect — unless a darkness-family effect on any member suppresses the party's light while it runs (the printed radii swallow a marching party). Darkness with blocks_infravision disables infravision too.

Returns:

Type Description
tuple[bool, bool]

The pair of party-level light facts.

bright_light

bright_light() -> bool

Whether the party carries daylight-bright light (continual light's data).

RAW modifies the wandering chance for "bright light sources"; osrlib adopts the reading that the torch/lantern flame is the baseline the printed 1-in-6 already assumes, so only brightness == "daylight" counts.

member_has_infravision

member_has_infravision(member: Character) -> bool

Whether one member sees in the dark: the class tag or a spell effect.

party_valuation_cp

party_valuation_cp() -> int

The party's treasure valuation in copper pieces — the award's exact unit.

All members count, including the dead (their carried treasure that made it back is the party's recovery): coin value in cp plus every valuable's value_gp. Magic items and mundane equipment count zero — magical treasure grants no XP per RAW, and mundane-gear salvage sits below the simulation floor by design.

snapshot_treasure

snapshot_treasure() -> None

Record the departure valuation — EnterDungeon's bookkeeping.

award_adventure_xp

award_adventure_xp() -> list[Event]

The end-of-adventure award: defeated monsters plus the valuation delta.

The treasure XP is the delta between the party's valuation now and the departure snapshot — floored to gp once from the cp total, never negative (clamped at zero: a party that lost money learned nothing monetarily). The total divides evenly among living members (floor division, remainder dropped — RAW divides evenly and B/X arithmetic is integer) and applies through apply_xp directly (a command whose handler executed further commands would double-log; AwardXP remains the referee and game surface). Dead members' recovered treasure counts toward the pool; dead members receive no share. A TPK never awards — no one returned. The defeated-monsters ledger clears and the next departure snapshots anew.

award_immediate_xp

award_immediate_xp(amount: int) -> list[Event]

The immediate timing's division: apply one award pool now.

Same division and events as the return award: evenly among living members, floor division, remainder dropped.

view

view(visibility: Visibility) -> PlayerView | RefereeView

Return the projection for a visibility level.

The player view is an enumerated whitelist safe to show at the table; the referee view is the full state minus RNG internals. Neither carries the master seed — it lives only in the save.

Parameters:

Name Type Description Default
visibility Visibility

PLAYER for the safe whitelist, REFEREE for everything but RNG internals.

required

Returns:

Type Description
PlayerView | RefereeView

The frozen view.

Examples:

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
from osrlib.crawl.dungeon import DungeonSpec, LevelSpec
from osrlib.crawl.party import Party
from osrlib.crawl.session import GameSession

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

player = session.view(Visibility.PLAYER)
referee = session.view(Visibility.REFEREE)
assert player.mode == "exploring"
# The referee sees session flags; the player whitelist has no such field.
assert "flags" in referee.state
assert "flags" not in player.model_dump()
# Neither view leaks the master seed — it lives only in the save.
assert "master_seed" not in referee.state

Listener

Bases: Protocol

The extension-point protocol: games register listeners on the session.

Listeners never mutate game state — they react by executing ordinary commands. handle receives the accumulated events of the command (earlier listeners' included) and the listener's own state snapshot, and returns the events to append plus the new state (snapshotted into saves under key).

key instance-attribute

key: str

handle

handle(events: Sequence[Event], state: dict) -> tuple[list[Event], dict]

React to one command's events.