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
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
Stream key for encounter-procedure draws: surprise, distance, reaction, distraction.
EXPLORATION_STREAM
module-attribute
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
Stream key for the action policy's draws — a policy change never shifts combat draws.
WANDERING_STREAM
module-attribute
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.
DefeatedMonsterRecord
DeprivationState
Bases: BaseModel
One member's food and water deprivation counters (worse track applies).
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.
metadata
property
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 |
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
Live entities by id: party members (marching order), then monsters, then NPCs.
combatant
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 |
member
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
|
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 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 |
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
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
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
Record the departure valuation — EnterDungeon's bookkeeping.
award_adventure_xp
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
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
|
|
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).