Skip to content

osrlib.crawl.gates

Authored gates: the condition vocabulary, the gate model, and pure evaluation.

A gate is an authored predicate the engine checks when the party attempts something, such as opening a door or taking a stair.

Where a gate sits. You write a GateSpec into the requires field of a DoorSpec or a TransitionSpec, inside the dungeon geometry of an Adventure. A gate hangs nowhere else. The exploration handlers behind GameSession.execute evaluate it as the last validation step of OpenDoor, ForceDoor, and UseStairs. A gate that refuses produces the rejection exploration.door.gate_refused or exploration.transition.gate_refused, which includes the authored refusal beat, and no event at all. A gate that opens puts its success beat on the command's own event: a DoorEvent for a door, a LocationEnteredEvent for a transition that crosses into a new level or dungeon. A gate that charges a toll reports it as an ItemConsumedEvent ahead of that event.

A gate is stateless content. condition_holds reads live session state at the moment of the attempt and stores nothing, so a key that gets dropped or sold stops opening its door.

The condition vocabulary is a discriminated union that grows additively:

  • HasItemCondition: some party member's carried inventory contains an item with that catalog id (Inventory.carried_item is the matching rule, equipped slots included). With consumes=True, the successful command takes one instance from the first holder in marching order.
  • FlagEqualsCondition: a session flag, which your game writes with SetFlag, is set to a value.
  • EffectActiveCondition: an active effect of that kind is attached to a party member, so you can ask for the talisman invoked rather than merely carried.

Gates and locks are separate layers. A lock is stateful mechanics with dice and per-character memory. A gate is a stateless authored predicate. A door with both requires both. Evaluation draws no dice, costs no game time, and mutates nothing, which is what lets a gate refusal be an ordinary command rejection.

Use a gate when the party has to satisfy a condition to get through. When you want something to happen because the party already did something, author a trigger (TriggerSpec) instead. The guide Gates, triggers, and quests walks all three from an adventure document you can run.

ConditionSpec module-attribute

ConditionSpec = Annotated[
    HasItemCondition | FlagEqualsCondition | EffectActiveCondition, Field(discriminator="condition_type")
]

The condition union, discriminated on condition_type.

Its members are HasItemCondition (has_item), FlagEqualsCondition (flag_equals), and EffectActiveCondition (effect_active). Annotate a field with this alias when you write your own model that holds authored conditions. Pydantic then picks the member from the condition_type value in the document. New kinds join the union additively, and the discriminator values are wire values that appear in every document that includes a gate.

A condition is read on the condition of a GateSpec, on the conditions of a TriggerSpec, and on the conditions of a quest's TriggerClause, and each of those evaluates through condition_holds.

EffectActiveCondition

Bases: BaseModel

An active effect of kind is attached to some party member.

Use it when the talisman has to be invoked rather than merely carried: the item grants an effect, and the gate asks for the effect. The session's effects ledger (EffectsLedger) is what gets read. Effects attached to a location never count, because the test is for an effect on a party member.

Examples:

from osrlib.crawl.gates import EffectActiveCondition

warded = EffectActiveCondition(kind="ward_of_the_deep")
assert warded.condition_type == "effect_active"

condition_type class-attribute instance-attribute

condition_type: Literal['effect_active'] = 'effect_active'

The discriminator value, effect_active. It appears in every document that includes this condition, and you never set it yourself.

kind class-attribute instance-attribute

kind: str = Field(min_length=1)

The effect kind to look for. Effect kinds are an open, data-driven vocabulary ("fatigue", or the kind of an EffectDefinition you wrote), so the field is a free string and nothing validates it against a catalog.

FlagEqualsCondition

Bases: BaseModel

A session flag holds value: the lever that opens the portcullis.

Your game writes flags with SetFlag, and a trigger's consequence can write one too, so this is how a lever in one room governs a door in another. The comparison runs through flag_values_equal and it is strict: a key that was never written equals nothing, False included, and a stored True never matches an authored 1.

Examples:

from osrlib.crawl.gates import FlagEqualsCondition

raised = FlagEqualsCondition(key="crypt.portcullis", value="raised")
assert raised.condition_type == "flag_equals"

condition_type class-attribute instance-attribute

condition_type: Literal['flag_equals'] = 'flag_equals'

The discriminator value, flag_equals. It appears in every document that includes this condition, and you never set it yourself.

key class-attribute instance-attribute

key: str = Field(min_length=1)

The flag name to read from the session flag store.

value instance-attribute

value: str | int | bool

The value the flag has to hold. The comparison keeps types apart, so author the same type your SetFlag writes.

GateSpec

Bases: BaseModel

A condition guarding an attempt, with the authored text for both outcomes.

Put one in the requires field of a DoorSpec or a TransitionSpec when you build the dungeon, and the engine checks it every time the party tries that door or stair. A refused attempt returns the refusal beat inside its rejection and costs the party nothing: no dice, no game time, no item, and no change to the door. A successful attempt puts the success beat on the command's own event.

A door standing open is not checked, so a gate on a door your game opens with SetDoorState applies again only once the door closes.

Examples:

from osrlib.crawl.gates import GateSpec, HasItemCondition
from osrlib.crawl.narrative import NarrativeBlock

sentinel = GateSpec(
    condition=HasItemCondition(item_id="brass_key"),
    narrative=NarrativeBlock(
        refusal="The bronze sentinel folds its arms. Brass, it says. Brass or nothing.",
        success="The brass key turns in the sentinel's palm and the door swings wide.",
    ),
)
assert sentinel.condition.item_id == "brass_key"

condition instance-attribute

condition: ConditionSpec

The predicate the party has to satisfy. Exactly one condition, evaluated live at the moment of the attempt. To ask for two things at once, put the second condition on a trigger that writes the flag this gate reads.

narrative class-attribute instance-attribute

narrative: NarrativeBlock | None = None

The authored text for both outcomes. A gate reads two beats of the block, refusal and success; the rest are left alone. None gates the way with no words at all, and the rejection then includes no refusal text.

HasItemCondition

Bases: BaseModel

The party carries an item with item_id, equipment or magic item alike.

Write this into the condition of a GateSpec for the door that needs a key, and into the conditions of a TriggerSpec or a quest's TriggerClause to narrow a firing to a party that is still carrying something. Any member's carried inventory satisfies it, equipped slots included, because carrying is the test. The party carries its dead and their packs, so a key in a dead member's pack still counts.

item_id has to resolve against the effective equipment catalog, which is the shipped catalog plus the adventure's own bundled items, or against the magic-item catalog. A gate naming an id that is in neither catalog fails adventure validation.

Examples:

from osrlib.crawl.gates import HasItemCondition

brass_key = HasItemCondition(item_id="brass_key")
assert not brass_key.consumes  # carrying is enough, and nothing is taken

condition_type class-attribute instance-attribute

condition_type: Literal['has_item'] = 'has_item'

The discriminator value, has_item. It appears in every document that includes this condition, and you never set it yourself.

item_id class-attribute instance-attribute

item_id: str = Field(min_length=1)

The catalog id to look for, from the equipment catalog (shipped items plus the adventure's bundled ones) or the magic-item catalog.

consumes class-attribute instance-attribute

consumes: bool = False

Whether a success takes the item. True makes it a toll: one instance leaves the first holder in marching order every time the gated command succeeds, so a door that swings shut needs another key. A trigger's or a quest's conditions reject True at parse, because they observe an event that has already happened and have no attempt of their own to charge against.

condition_holds

condition_holds(
    condition: ConditionSpec,
    *,
    members: Sequence[Character],
    flags: Mapping[str, str | int | bool],
    ledger: EffectsLedger
) -> bool

Evaluate one condition against live session state.

Every surface that asks whether an authored condition holds asks through here: the gate on a door or a stair, a TriggerSpec's conditions, and a quest TriggerClause's. You rarely call it yourself, because the engine calls it for you at the moment of an attempt and the Interpreter calls it at the moment of a match. Call it directly when your own listener wants to ask the same question the engine asks, or to check an authored document against a session in a test.

The call draws no dice, advances no clock, mutates nothing, and stores nothing. Every walk is over an ordered list, the party in marching order and each inventory in carried order, so the answer is deterministic. The member domain is every party member, living or dead, because the party carries its dead and their packs.

Parameters:

Name Type Description Default
condition ConditionSpec

The condition to evaluate, one member of ConditionSpec.

required
members Sequence[Character]

The party, in marching order, from session.party.members.

required
flags Mapping[str, str | int | bool]

The session flag store, from session.flags.

required
ledger EffectsLedger

The session's effects ledger, from session.ledger.

required

Returns:

Type Description
bool

True when the condition holds right now.

Examples:

from osrlib.core.effects import EffectsLedger
from osrlib.crawl.gates import FlagEqualsCondition, condition_holds

condition = FlagEqualsCondition(key="portcullis", value="raised")
flags = {"portcullis": "raised"}
assert condition_holds(condition, members=[], flags=flags, ledger=EffectsLedger())

first_holder

first_holder(members: Sequence[Character], item_id: str) -> Character | None

Return the first member in marching order carrying an item with item_id.

This is the consumption target of a has_item toll, and it matches exactly what condition_holds tests, so the member it names is the one the engine charges. Call it when your own code has to name the holder the engine would charge. The search is over every member, living or dead, and over each inventory in carried order, equipped slots included.

Parameters:

Name Type Description Default
members Sequence[Character]

The party, in marching order, from session.party.members.

required
item_id str

The catalog id to look for.

required

Returns:

Type Description
Character | None

The member, or None when nobody carries one.

Examples:

from osrlib.crawl.gates import first_holder

assert first_holder([], "brass_key") is None

flag_values_equal

flag_values_equal(stored: str | int | bool, expected: str | int | bool) -> bool

Compare two flag values the one strict way the engine compares them.

The test is equality plus matching boolness. True == 1 in Python, so a flag your game set to True does not satisfy an authored 1, and an authored True does not match a stored 1: they are different values in an authored document even though Python calls them equal. Call it when your own code has to answer the same question about a flag that the engine answers.

Every surface that asks whether a flag holds a value asks through here, which is why FlagEqualsCondition on a gate and FlagSetPattern on a trigger can never disagree about what equality means.

Parameters:

Name Type Description Default
stored str | int | bool

The value the flag store holds, or the value a FlagSetEvent reports written.

required
expected str | int | bool

The value the author wrote.

required

Returns:

Type Description
bool

True when the two are the same value.

Examples:

from osrlib.crawl.gates import flag_values_equal

assert flag_values_equal("open", "open")
assert not flag_values_equal(True, 1)
assert not flag_values_equal(1, True)