Skip to content

osrlib.crawl.interpreter

The interpreter: the listener that plays an adventure's authored triggers and quests.

Interpreter is an ordinary listener the game registers on its session (GameSession.register_listener). It watches the events of every accepted command, matches them against the adventure's TriggerSpecs and QuestSpecs, and acts the only way anything outside the engine may act: by executing ordinary referee commands, each stamped with the trigger or quest it acted for.

That discipline is what keeps an authored game replayable. The interpreter emits no events of its own and remembers nothing between commands, so a replay — which runs with no listeners at all — rebuilds the same world by re-executing the same log. Every effect a trigger or a quest has is a command in that log, and every one of those commands says whose idea it was.

Interpreter

Interpreter(session: GameSession)

Plays an adventure's authored triggers and quests by issuing referee commands.

Register one, once, on a session that has already been built:

session.register_listener(Interpreter(session))

Registering twice fires everything twice — the same rule every listener follows — and a session restored from a save needs the registration again, because listeners are code and a save carries data. Nothing migrates: the interpreter's slot in listener_state is empty and stays empty forever.

What it does with a command's events. It walks them in the order they happened and, per event, the adventure's triggers in document order and then its quests in document order — one rule, and the only order there is. A trigger matches when its pattern fits the event, its fired-state allows it (once-only unless repeatable), and every one of its conditions holds against session state right now. A match fires immediately, before the walk moves on, so a later trigger's conditions see what an earlier firing has already changed.

What a firing issues, all of it stamped source="trigger:{id}":

  1. MarkTriggerFired, carrying the fired beat. The mark goes in first, which is what makes once-only safe against a trigger whose own consequences would match it again.
  2. The consequences, in authored order, with @party and @first expanded to the living members they name — so the log records concrete character ids and replays exactly.
  3. AddJournalEntry when the trigger's narrative carries a journal form, last, so the beat is stamped with the clock the consequences left behind.

What a quest walk issues, all of it stamped source="quest:{id}". A quest clause (TriggerClause) is matched exactly the way a trigger is — the same patterns, the same live conditions — and the walk goes:

  1. An inactive quest whose activation clause matches gets ActivateQuest, and the walk carries on into the objectives of the quest it just activated: the same event that starts a quest can finish something in it.
  2. An active quest's objectives walk in authored order. A hidden, unrevealed, incomplete objective whose reveal_when matches gets RevealObjective; an incomplete objective whose when matches gets CompleteObjective. An objective that completes without ever being revealed needs no reveal — completing shows it.
  3. The moment a completion lands, the quest's completion rule is checked against live state (all or any), and a satisfied rule gets CompleteQuest followed by the rewards in authored order, selectors expanded exactly as a trigger's consequences are. On a quest that concludes the adventure the session is in victory before the first reward is issued, which is why a reward that would resume play there drops with a note.

Everything is evaluated as the walk goes: a flag an earlier firing wrote satisfies a later clause's condition in the same batch, and a quest completed earlier in the walk is completed for everything after it.

Where the interpreter's discipline stops and the referee's ruling begins. The completion rule is checked only after a completion the interpreter itself issued, and no pattern matches the quest events, so a referee who completes the last objective by hand completes the quest by hand too. For the same reason a hand-driven CompleteQuest grants no rewards: rewards are this listener reading the quest, and what a replay re-executes is the reward commands themselves.

When something does not work out, the run continues and the log says why. A rejected consequence or reward is dropped on its own — a spawn that meets an open encounter, a grant to a character who is not there — and a RecordNote records the trigger or quest, the slot's position and type, and the rejection. If a wipe mid-cascade ends the session, the remaining commands land or drop by the ordinary rules of a terminal mode. And a cascade is bounded: what a firing or a quest advancement issues is one deeper than the event that caused it, matching stops below depth five, and every advancement the bound suppresses is recorded as a note instead of being issued — no state moves, so a once-only trigger cut short here is still fireable later, and a suppressed quest advancement waits for its clause to match again. Clauses are edge-triggered on both surfaces: the suppressed edge is gone.

What it never does. It returns no events, because everything it causes is already logged by the commands it executed, and it keeps no memory between commands. Read what a trigger or a quest did from the command log, the journal, session.fired_triggers, and session.quests, all of which a replay rebuilds.

Bind the interpreter to the session it watches and issues commands through.

Parameters:

Name Type Description Default
session GameSession

The session; its adventure's triggers and quests are read once here, being frozen content.

required

key class-attribute instance-attribute

key = 'osrlib.interpreter'

The listener key; its state entry exists because registration creates one, and is the empty dict for the life of the session.

handle

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

Match one command's events and act on what they crossed.

Parameters:

Name Type Description Default
events Sequence[Event]

The command's accumulated events, in the order they happened.

required
state dict

The listener's state slot, always the empty dict.

required

Returns:

Type Description
list[Event]

No events and the empty state — everything the interpreter does is a

dict

command it executed, and it remembers nothing.