osrlib.crawl.dungeon
The dungeon: the grid you author, and the overlay play writes over it.
This module holds both halves of a dungeon. DungeonSpec and
everything under it (levels, edges, areas, features, traps, transitions) is authored content: frozen
models you build, hand to an Adventure, and never change again.
DungeonState is the mutable overlay the running session writes
alongside it: which cells the party has walked, which doors stand open, which traps have gone off,
what has been dropped on the floor, and where the party is standing. The overlay is what a save file
carries, and it refers to the content by string references rather than by object, so it serializes
flat.
You build the geometry here and assemble it into an adventure in
osrlib.crawl.adventure. You never construct DungeonState yourself:
GameSession.new makes one, and you read it through the
session's views. The long form, with a complete program you can run, is the guide
Building an adventure.
The geometry, which every member here assumes: a level is a grid of 10-foot cells addressed (x, y),
with x increasing east and y increasing south from (0, 0) in the northwest corner. Walls are
the default, and you declare the exceptions. An edges map holds one entry per physical edge that is
something other than wall, keyed by edge_key so the boundary
between two cells has exactly one entry no matter which side you name it from. An edge with no entry
is wall, and so is the level boundary.
Typical usage:
from osrlib.crawl.dungeon import Direction, DungeonSpec, Edge, EdgeKind, LevelSpec, edge_key
# A two-cell corridor running west to east, entered at the west end.
corridor = LevelSpec(
number=1,
width=2,
height=1,
entrance=(0, 0),
edges={edge_key((0, 0), Direction.EAST): Edge(kind=EdgeKind.OPEN)},
)
crypt = DungeonSpec(id="crypt", name="The Old Crypt", levels=(corridor,))
print(edge_key((0, 0), Direction.EAST))
# 1,0:west
print(crypt.level(1).edge((0, 0), Direction.NORTH).kind)
# wall
Position
module-attribute
A cell address on a level's grid: (x, y).
x increases east and y increases south from (0, 0) in the level's northwest corner, and one
cell is 10 feet on a side. Write one as a plain tuple, (3, 0). A position is only meaningful
against a particular level, and one that names a cell off the grid is out of bounds rather than
invalid: LevelSpec.in_bounds is how you ask, and
validate_adventure is what catches an authored one
that lands outside.
AreaSpec
Bases: BaseModel
A keyed room or cave: a named region of cells with content bound to it.
Areas are how you key a dungeon. Each one covers some cells of a
LevelSpec, and cells no area covers are corridor. Entering
any cell of an area is what brings its content into play: the encounter spawns, the trap gets its
spring roll, the treasure rolls, and your front end shows the description.
Attributes:
| Name | Type | Description |
|---|---|---|
id |
str
|
The area's id, unique across its level. |
name |
str
|
The room's name. |
description |
str
|
Prose for your front end. |
cells |
tuple[Position, ...]
|
The cells the area covers. |
encounter |
KeyedEncounter | None
|
The monsters waiting here. |
features |
tuple[FeatureSpec, ...]
|
The keyed things in the room. |
trap |
TrapSpec | None
|
A room trap over the whole area. |
treasure |
AreaTreasureSpec | None
|
Generated treasure with nothing guarding it. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Examples:
from osrlib.crawl.dungeon import AreaSpec, KeyedEncounter, KeyedMonster
guard_post = AreaSpec(
id="guard_post",
name="Guard post",
description="Two goblins crouch over a game of knucklebones.",
cells=((3, 0),),
encounter=KeyedEncounter(monsters=(KeyedMonster(template_id="goblin", count_fixed=2),)),
)
print(guard_post.cells)
# ((3, 0),)
id
instance-attribute
id: str
The area's id, which has to be unique across the level. Events carry it, triggers match on it, and the state overlay records the area's encounter and treasure against it.
name
class-attribute
instance-attribute
name: str = ''
The room's name, for your front end to show: "Guard post", "The abbot's cell".
description
class-attribute
instance-attribute
description: str = ''
Prose your front end shows when the party walks in. Events carry the area's id rather than its
words, so the text lives here and the front end looks it up. For prose split by audience, see
NarrativeBlock.
cells
class-attribute
instance-attribute
The cells the area covers, at least one. They need not be contiguous, though a room usually
is. Every one has to be on the level's grid.
AreaSpec.cells[0] is where a generated hoard lands.
encounter
class-attribute
instance-attribute
encounter: KeyedEncounter | None = None
The monsters waiting in the room, or None for an empty one. See
KeyedEncounter.
features
class-attribute
instance-attribute
features: tuple[FeatureSpec, ...] = ()
The keyed things in the room: caches, tricks, and your own content. A feature here may leave
its cell unset, which binds it to the area rather than to one square.
trap
class-attribute
instance-attribute
trap: TrapSpec | None = None
A trap over the whole area, or None. It has to be a room trap, which springs when the party
enters a cell of the area or, with trigger="open", when a door of the area is opened.
treasure
class-attribute
instance-attribute
treasure: AreaTreasureSpec | None = None
Treasure the engine rolls on first entry, for a room with loot and nothing guarding it. A
room whose treasure belongs to its monsters uses the encounter's hoard flag instead.
AreaTreasureSpec
Bases: BaseModel
Treasure the engine rolls for an area that has no monsters guarding it.
Put one on an AreaSpec when you want the room to hold loot but
do not want to choose it. It rolls the first time the party enters the area and lands as a cache
on the floor, which the party then picks up with
TakeTreasure. Generated treasure is never trapped. Trapping
is authored, through a FeatureSpec with a trap on it.
For an area whose treasure is a monster's hoard, use a
KeyedEncounter and its hoard flag instead: that is the
lair treasure, and it comes with the monsters.
Attributes:
| Name | Type | Description |
|---|---|---|
letters |
tuple[str, ...]
|
The treasure type letters to roll. |
unguarded |
bool
|
Whether to roll the level's unguarded-treasure band instead. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If both |
Examples:
from osrlib.crawl.dungeon import AreaTreasureSpec
print(AreaTreasureSpec(letters=("C",)).unguarded)
# False
letters
class-attribute
instance-attribute
One or more B/X treasure type letters, like ("C",). Each letter is its own hoard table, and the
full list is the treasure type index. Set this or unguarded, not
both.
unguarded
class-attribute
instance-attribute
unguarded: bool = False
Whether to roll the dungeon level's unguarded-treasure band instead of naming letters. That
band is the SRD's own table for treasure lying about with nothing watching it, and it scales with
the level number. Set this or letters, not both.
Direction
Bases: StrEnum
The four grid directions the party faces and moves in.
This is the direction vocabulary the whole crawl uses: which way
MoveParty steps, which side of a cell
OpenDoor works on, which way a transition faces the party on
arrival. There is no up or down here. Between levels is a
TransitionSpec.
The wire values are lowercase and they serialize into commands, events, and saves, so changing
one is a schema_version bump.
SOUTH
class-attribute
instance-attribute
Increasing y: toward the bottom of the map.
vector
property
The (dx, dy) step for one cell in this direction.
Add it to a position to get the neighbour, or call
step, which does the addition for you.
opposite
property
opposite: Direction
The reverse direction.
The direction you came from is the opposite of the one you went. A transition's to_facing
and a door seen from the far side are both read this way.
DoorSpec
Bases: BaseModel
A door on an edge, as you authored it.
Put one on an Edge whose kind is door. Everything here is the
door's starting condition, and play never writes back to it. What the party does to the door goes
into DoorState in the overlay instead, which is where you
read whether a door is open right now.
Attributes:
| Name | Type | Description |
|---|---|---|
kind |
Literal['normal', 'secret']
|
Whether the door is visible from the start or has to be found. |
stuck |
bool
|
Whether the door needs forcing before it opens. |
locked |
bool
|
Whether the door needs a key or a thief before it opens. |
starts_open |
bool
|
Whether the door stands open when the party first arrives. |
requires |
GateSpec | None
|
An authored condition the party must satisfy to open the door. |
Examples:
from osrlib.crawl.dungeon import DoorSpec, Edge, EdgeKind
vault = Edge(kind=EdgeKind.DOOR, door=DoorSpec(locked=True))
print(vault.door.locked, vault.door.kind)
# True normal
kind
class-attribute
instance-attribute
kind: Literal['normal', 'secret'] = 'normal'
"normal" for a door the party can see, "secret" for one it cannot. A secret door is
invisible until a successful secret-door search finds it, which marks discovered on the door's
overlay entry. Until then the party cannot open it, listen at it, or walk through it, and the
edge reads to the player as wall.
stuck
class-attribute
instance-attribute
stuck: bool = False
locked
class-attribute
instance-attribute
locked: bool = False
Whether the door is locked. The engine refuses to open a locked door until something unlocks it:
PickLock by a thief, or a referee
SetDoorState. Forcing still works, since a locked door can
be broken open.
starts_open
class-attribute
instance-attribute
starts_open: bool = False
Whether the door already stands open when the party first reaches it. An authored-open door stays open: the rule that a door swings shut behind the party applies only to doors the party itself opened.
requires
class-attribute
instance-attribute
requires: GateSpec | None = None
An authored gate on opening the door, or None for a door anyone may open. A gate
(GateSpec) is a stateless condition the engine checks whenever
the party tries to open or force the door, after every ordinary refusal has passed. It is
independent of locked: a door with both needs both, PickLock
addresses only the lock, and SetDoorState writes the
overlay without consulting the gate at all. A door standing open lets the party through
unchecked, because the gate guards the opening rather than the doorway, and applies again once
the door closes.
DoorState
Bases: BaseModel
What has happened to one door: open, wedged, discovered, unlocked.
This is the mutable half of a door. DoorSpec is how you
authored it and never changes. This records what the party has done since. Get one from
DungeonState.door with the door's
edge_ref.
Attributes:
| Name | Type | Description |
|---|---|---|
open |
bool
|
Whether the door stands open. |
wedged |
bool
|
Whether a spike holds it. |
discovered |
bool
|
Whether a secret door has been found. |
unlocked |
bool
|
Whether a lock has been dealt with. |
opened_by_party |
bool
|
Whether the party is the one that opened it. |
open
class-attribute
instance-attribute
open: bool = False
Whether the door stands open right now. A door with starts_open set begins here as True.
The party walks through an open door without opening it again.
wedged
class-attribute
instance-attribute
wedged: bool = False
Whether an iron spike holds the door, from WedgeDoor. A
wedged door does not swing shut behind the party, which is the point of carrying spikes.
discovered
class-attribute
instance-attribute
discovered: bool = False
Whether a secret door has been found. A secret door does nothing for the party until a
successful secret-door Search sets this. Until then the edge reads
as wall. A normal door ignores it.
unlocked
class-attribute
instance-attribute
unlocked: bool = False
Whether a locked door has been dealt with, by
PickLock or by a referee
SetDoorState. A door stays unlocked once it is: relocking
is a referee's write, not something closing the door does.
opened_by_party
class-attribute
instance-attribute
opened_by_party: bool = False
Whether the party is the one that opened this door. The swing-shut rule reads it: only doors the party opened, by whatever means, swing closed behind it, while a door you authored open stays open.
DropPile
Bases: BaseModel
What is lying on one cell's floor, waiting to be picked up.
Piles are where loose goods end up: gear the party dropped with
DropItems, what the monsters left at the end of a fight, and
the part of a cache the party could not carry. The party picks a pile up with
TakeTreasure naming the reserved feature id "pile",
which is why no authored feature may use that id. Goods the party scatters as bait while it runs
from a pursuer are the exception: those are gone rather than dropped here.
The session keeps these in DungeonState.piles keyed by
cell_ref. Piles form only where the party has stood, so a pile
is always on a cell the party knows about.
Attributes:
| Name | Type | Description |
|---|---|---|
items |
list[DroppedItem]
|
The ordinary items in the pile. |
coins |
Coins
|
The coins in the pile. |
valuables |
list[ValuableInstance]
|
The gems and jewellery in the pile. |
magic_items |
list[MagicItemInstance]
|
The magic items in the pile. |
items
class-attribute
instance-attribute
items: list[DroppedItem] = []
The ordinary items lying here, stacked by template id.
coins
class-attribute
instance-attribute
The coins lying here, by denomination.
valuables
class-attribute
instance-attribute
valuables: list[ValuableInstance] = []
The gems and jewellery lying here, each keeping the id it already had.
magic_items
class-attribute
instance-attribute
magic_items: list[MagicItemInstance] = []
The magic items lying here, each keeping its own charges or quantity.
DroppedItem
Bases: BaseModel
One stack of an ordinary item lying on the floor.
A DropPile holds these. Identical items stack, so five iron
spikes are one entry with a quantity rather than five entries.
Attributes:
| Name | Type | Description |
|---|---|---|
item_id |
str
|
Which item this is. |
quantity |
int
|
How many are in the stack. |
item_id
instance-attribute
item_id: str
The item's template id, resolving against the session's effective equipment catalog.
DungeonSpec
Bases: BaseModel
A dungeon: one or more levels joined by transitions.
This is the unit an Adventure holds and the unit
EnterDungeon names. Build its levels first, then wrap
them here, then put the dungeon in an adventure beside its town. An adventure may hold several,
and a TransitionSpec may point at another one's id, so a
stair can lead out of one dungeon and into the next.
Nothing here says which level is the way in. Some level needs an entrance, and
validate_adventure is what checks that one does.
Attributes:
| Name | Type | Description |
|---|---|---|
id |
str
|
The dungeon's id. |
name |
str
|
The dungeon's name. |
levels |
tuple[LevelSpec, ...]
|
Its levels. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If two levels carry the same |
Examples:
from osrlib.crawl.dungeon import DungeonSpec, Edge, EdgeKind, LevelSpec
corridor = LevelSpec(number=1, width=2, height=1, entrance=(0, 0), edges={"1,0:west": Edge(kind=EdgeKind.OPEN)})
crypt = DungeonSpec(id="crypt", name="The Old Crypt", levels=(corridor,))
print(crypt.level(1).width)
# 2
id
instance-attribute
id: str
The dungeon's id, unique within the adventure. EnterDungeon,
the town's travel_turns map, transitions between dungeons, and every state-overlay reference
name it.
name
class-attribute
instance-attribute
name: str = ''
The dungeon's name, for your front end to show: "The Old Crypt".
levels
class-attribute
instance-attribute
The dungeon's levels, at least one, with unique numbers. Look one up with
level rather than by position. The order does matter
in one place: EnterDungeon lands the party on the first
level in this tuple that has an entrance, facing north.
level
Return the level with number.
Use this to turn a level number out of a command, an event, or a transition back into the
level it names, rather than searching levels yourself.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
number
|
int
|
The 1-based level number. |
required |
Returns:
| Type | Description |
|---|---|
LevelSpec
|
The level spec. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If no level has that number. The message names the dungeon and the number. |
DungeonState
Bases: BaseModel
Everything play has changed: the overlay the session writes over frozen content.
The adventure you authored never changes. This is where the running game records what happened to
it, and it is the part a save file contains. GameSession.new
makes one, the command handlers write it, and you read it through
build_player_view or
build_referee_view rather than reaching in, so you get
the right visibility for your audience.
References into content are strings rather than objects, so the overlay serializes flat and a
save never has to carry the adventure with it. They come in four shapes: "{dungeon}:{level}"
keys the explored and seen maps, edge_ref keys doors,
"{dungeon}:{level}:{area_or_feature_id}" keys areas and authored features, and
cell_ref keys drop piles.
Two maps of cells look alike and are not. explored is the party's footprint, the cells it has
physically walked, and it is what movement cost reads. seen is the party's map memory, the
cells its own light has shown it, and it is read by the player projection only so a front end's
automap can keep a room the party looked into and walked past.
The attempt memories are here rather than in a procedure's local variables because they are game state that has to survive a save: who has already listened at this door, who has already searched this cell for this kind of thing, and which thief failed this lock and at what level.
Attributes:
| Name | Type | Description |
|---|---|---|
location |
PartyLocation
|
Where the party is. |
explored |
dict[str, list[Position]]
|
Cells the party has walked. |
seen |
dict[str, list[Position]]
|
Cells the party has looked at. |
doors |
dict[str, DoorState]
|
What has happened to each door. |
sprung_traps |
list[str]
|
Traps that have gone off. |
removed_traps |
list[str]
|
Traps a thief has taken out. |
found_traps |
list[str]
|
Traps the party knows about. |
found_tricks |
list[str]
|
Construction tricks the party has found. |
emptied_caches |
list[str]
|
Authored caches the party has emptied. |
piles |
dict[str, DropPile]
|
What is lying on the floor, by cell. |
generated_caches |
dict[str, GeneratedCache]
|
Treasure the engine rolled, by cache id. |
generated_treasure_areas |
list[str]
|
Areas whose treasure has already rolled. |
resolved_encounters |
list[str]
|
Areas whose keyed encounter is over. |
listen_attempts |
dict[str, list[str]]
|
Who has listened at each door. |
search_attempts |
dict[str, list[str]]
|
Who has searched each cell for what. |
inspect_attempts |
dict[str, list[str]]
|
Who has inspected each cache for traps. |
removal_attempts |
dict[str, list[str]]
|
Who has tried to remove each trap. |
lock_failures |
dict[str, dict[str, int]]
|
Which thief failed each lock, and at what level. |
location
class-attribute
instance-attribute
location: PartyLocation = PartyLocation(kind='town')
Where the party is standing, or that it is in town. A new session starts in town.
explored
class-attribute
instance-attribute
The cells the party has physically entered, keyed "{dungeon}:{level}". This is the footprint
movement cost reads: stepping back into an explored cell is three times as fast as breaking new
ground. Drop-pile visibility reads it too.
seen
class-attribute
instance-attribute
The cells the party's light has shown it, keyed "{dungeon}:{level}". This is map memory,
read by the player projection only, and it never affects movement cost. Cells append in sorted
(x, y) order so a save is byte-identical across runs.
doors
class-attribute
instance-attribute
sprung_traps
class-attribute
instance-attribute
The traps that have gone off, by area or feature reference. A sprung trap is done: it never rolls again.
removed_traps
class-attribute
instance-attribute
The treasure traps a thief has taken out with
RemoveTreasureTrap, by feature reference. A removed
trap never rolls again either. Room traps never appear here, since nothing disarms one.
found_traps
class-attribute
instance-attribute
The traps the party knows about, by area or feature reference, from a successful
Search on a room trap or
InspectTreasure on a treasure trap. A found trap stops
taking its spring roll, which is what finding one gets you, and a treasure trap has to be here before
RemoveTreasureTrap will work on it.
found_tricks
class-attribute
instance-attribute
The construction tricks the party has found by searching, by feature reference.
emptied_caches
class-attribute
instance-attribute
The authored caches the party has emptied, by feature reference. An emptied cache gives
nothing more. Engine-rolled caches are removed from generated_caches outright instead of being
listed here.
piles
class-attribute
instance-attribute
generated_caches
class-attribute
instance-attribute
generated_caches: dict[str, GeneratedCache] = {}
Treasure the engine rolled, keyed by a minted cache id like "cache-0001". A
HoardGeneratedEvent announces the id, TakeTreasure names
it, and emptying one deletes the entry.
generated_treasure_areas
class-attribute
instance-attribute
The areas whose AreaTreasureSpec has already rolled,
by area reference, so entering a room twice does not double its loot.
resolved_encounters
class-attribute
instance-attribute
The areas whose keyed encounter is over, by area reference. The room stays clear afterwards.
listen_attempts
class-attribute
instance-attribute
Which characters have listened at each door: character ids keyed by
edge_ref. One try each, so a party cannot listen its way past
a bad roll by queueing up.
search_attempts
class-attribute
instance-attribute
Which characters have searched each cell for each kind of thing: character ids keyed
"{cell_ref}:{kind}", where the kind is what Search was looking
for. One try each per cell per kind.
inspect_attempts
class-attribute
instance-attribute
Which characters have inspected each cache for traps: character ids keyed by feature reference. One try each.
removal_attempts
class-attribute
instance-attribute
Which characters have tried to remove each trap: character ids keyed by feature reference. One try each, so a failed removal is final for that thief.
lock_failures
class-attribute
instance-attribute
Which thief failed which lock, and at what level: character id to level, keyed by
edge_ref. The level is why it records a number rather than a
flag. A thief who failed a lock may try it again once they have gained a level, and this is what
that comparison reads.
is_explored
Return whether the party has walked a cell.
Movement cost reads this: a step back into an explored cell costs a third of a step into new ground. Call it to shade a map, or to work out what a move is about to cost.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dungeon_id
|
str
|
The dungeon id. |
required |
level_number
|
int
|
The 1-based level number. |
required |
position
|
Position
|
The cell. |
required |
Returns:
| Type | Description |
|---|---|
bool
|
True when the party has physically entered that cell. A cell the party has only seen by its own light answers False. |
mark_explored
Mark a cell as walked.
The session calls this as the party arrives. Marking a cell twice changes nothing, so you can call it without checking first.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dungeon_id
|
str
|
The dungeon id. |
required |
level_number
|
int
|
The 1-based level number. |
required |
position
|
Position
|
The cell. |
required |
mark_seen
Mark cells as seen: the party's map memory of what its light has shown it.
Seen cells are read by the player projection only, so a front end's automap remembers a room
the party's light reached after the party has walked on. They never affect movement cost,
which reads the walked footprint through
is_explored, and they never affect
drop-pile visibility, which stays on walked cells because piles only form where the party has
stood.
Marking a cell twice changes nothing. New cells append in sorted (x, y) order, so a save is
byte-identical across runs regardless of the order you pass them in.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dungeon_id
|
str
|
The dungeon id. |
required |
level_number
|
int
|
The 1-based level number. |
required |
positions
|
Iterable[Position]
|
The cells to remember. Already-seen cells are skipped. |
required |
door
Return one door's live state, creating the entry the first time you ask.
Door entries are not created up front, so this is how you read one without worrying about whether the party has reached that door yet. The object it returns is the one in the map, so writing to it writes to the overlay.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
ref
|
str
|
The door's |
required |
Returns:
| Type | Description |
|---|---|
DoorState
|
The door's overlay entry, freshly created and all- |
Edge
Bases: BaseModel
One entry in a level's edges map: what stands on the boundary between two cells.
You write these into LevelSpec.edges keyed by
edge_key. Only the exceptions need entries, because an edge
with no entry is wall. Read one back with
LevelSpec.edge, which supplies a wall for anything
absent and for the level boundary.
Attributes:
| Name | Type | Description |
|---|---|---|
kind |
EdgeKind
|
What occupies the edge. |
door |
DoorSpec | None
|
The door, when |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Examples:
EdgeKind
Bases: StrEnum
What occupies an edge between two cells.
This is the kind of an Edge entry, and it is what makes the
boundary between two cells passable or not.
WALL
class-attribute
instance-attribute
Solid: the party cannot cross. This is also what a level reports for an edge with no entry at all, so you only write it when you want the wall stated outright.
DOOR
class-attribute
instance-attribute
A door stands here, and the entry contains the DoorSpec that
describes it. An edge of this kind must include one, and no other kind may.
FeatureSpec
Bases: BaseModel
A keyed thing in a room: a treasure cache, a construction trick, or your own content.
Features hang on an AreaSpec or straight on a
LevelSpec, and they are how a room holds something the party
can find and interact with. Stairs are not features. Those are
TransitionSpecs, and they have no second home.
A treasure_cache is the one kind the engine resolves on its own: the party opens it with
TakeTreasure naming the feature's id, and its contents go
into the party's hands. Hand-placed magic items are named here and instantiated when the cache is
emptied, so an item's own details (charges, quantities, whether a sword turns out to be sentient)
roll then, on the treasure stream, through
instantiate_magic_item. A construction_trick
is one of the SRD's weird architectural features, like a room that rotates or an illusory
passage: the party finds it by searching, and your front end says what it does. A custom
feature is yours entirely.
Attributes:
| Name | Type | Description |
|---|---|---|
id |
str
|
The feature's id, unique across its level. |
kind |
Literal['treasure_cache', 'construction_trick', 'custom']
|
Which sort of feature it is. |
description |
str
|
Prose for your front end. |
cell |
Position | None
|
The cell it sits on, or |
item_ids |
tuple[str, ...]
|
Ordinary items in a cache. |
magic_item_ids |
tuple[str, ...]
|
Magic items in a cache. |
coins |
Coins
|
Coins in a cache. |
valuables |
tuple[ValuableSpec, ...]
|
Named gems and jewellery in a cache. |
trap |
TrapSpec | None
|
A treasure trap guarding the cache. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Examples:
from osrlib.core.items import Coins
from osrlib.crawl.dungeon import FeatureSpec
chest = FeatureSpec(
id="altar_chest",
kind="treasure_cache",
description="A banded chest under the altar.",
cell=(3, 0),
item_ids=("rope_50", "holy_water"),
coins=Coins(gp=120),
)
print(chest.coins.gp, chest.item_ids)
# 120 ('rope_50', 'holy_water')
id
instance-attribute
id: str
The feature's id, which has to be unique across the level, counting features on the level and
on all of its areas together. TakeTreasure and the other
feature commands name it. The id "pile" is reserved for the drop pile on a cell, so authored
content may not use it, and validate_adventure refuses an adventure that does.
kind
instance-attribute
kind: Literal['treasure_cache', 'construction_trick', 'custom']
What sort of feature this is. "treasure_cache" is the only kind the engine resolves for the
party. "construction_trick" and "custom" contain content your front end interprets.
description
class-attribute
instance-attribute
description: str = ''
Prose your front end shows when the party finds the feature. Events carry the feature's id rather than its words, so the text lives here and the front end looks it up.
cell
class-attribute
instance-attribute
cell: Position | None = None
The cell the feature sits on. A feature listed on a level needs one. A feature listed on an
area may leave it None, which binds the feature to the whole area rather than to one square of
it.
item_ids
class-attribute
instance-attribute
Ordinary items in the cache, by template id. Any id the session's effective equipment catalog
holds works: a shipped id from load_equipment, listed in
the equipment id index, or one the adventure bundles on its items field.
magic_item_ids
class-attribute
instance-attribute
Magic items in the cache, by template id. Any id from
load_magic_items, listed in
the magic item id index. Adventures bundle no magic items of their own, so
only the shipped catalog resolves here.
coins
class-attribute
instance-attribute
Coins in the cache, by denomination.
valuables
class-attribute
instance-attribute
valuables: tuple[ValuableSpec, ...] = ()
Named gems and jewellery in the cache. See
ValuableSpec.
trap
class-attribute
instance-attribute
trap: TrapSpec | None = None
A trap guarding the cache, or None. It has to be a treasure trap, which springs when the
party opens the cache. A thief finds it with
InspectTreasure and takes it out with
RemoveTreasureTrap, one attempt each per
character.
GeneratedCache
Bases: BaseModel
Treasure the engine rolled and put on the floor, in the state overlay.
Authored caches are FeatureSpecs and never change. This is
the other kind: what a monster's lair hoard or an
AreaTreasureSpec produced when it rolled. The session
puts one in DungeonState.generated_caches under a minted
id like "cache-0001", announces it with a HoardGeneratedEvent, and removes it when the party
empties it with TakeTreasure naming that id.
Generated hoards are never trapped. Trapping treasure is something you author, not something a
roll produces, which is one of the choices listed in
the adaptations register, the page recording
where osrlib commits to one reading of an ambiguous rule or supplies a default behind a
Ruleset flag.
Attributes:
| Name | Type | Description |
|---|---|---|
cell_ref |
str
|
The cell the cache lies on. |
treasure_types |
tuple[str, ...]
|
The treasure type letters it rolled from. |
coins |
Coins
|
The coins in it. |
valuables |
list[ValuableInstance]
|
The gems and jewellery in it. |
magic_items |
list[MagicItemInstance]
|
The magic items in it. |
cell_ref
instance-attribute
cell_ref: str
Where the cache lies, as a cell_ref string. The party has
to be standing on that cell to take it.
treasure_types
class-attribute
instance-attribute
The treasure type letters the hoard rolled from, kept for display and for a referee who wants to see what the dice were asked. Empty for an unguarded roll, which names no letters.
coins
class-attribute
instance-attribute
The coins in the cache, by denomination.
valuables
class-attribute
instance-attribute
valuables: list[ValuableInstance] = []
The gems and jewellery in the cache, each with its own id.
magic_items
class-attribute
instance-attribute
magic_items: list[MagicItemInstance] = []
The magic items in the cache, each already rolled out with its charges or quantity.
KeyedEncounter
Bases: BaseModel
The monsters waiting in a keyed area, and what is already decided about them.
Put one on an AreaSpec. The monsters spawn the first time the
party enters any cell of the area, and the session moves into encounter mode: surprise, distance,
and reaction roll unless you have decided them here. The encounter resolves once, and the area
stays clear afterwards.
Attributes:
| Name | Type | Description |
|---|---|---|
monsters |
tuple[KeyedMonster, ...]
|
The monster lines, each a template and a count. |
alignment |
Alignment | None
|
A fixed alignment for templates that offer a choice. |
aware |
bool
|
Whether the monsters are expecting the party. |
stance |
ReactionResult | None
|
A fixed reaction, instead of a reaction roll. |
hoard |
bool
|
Whether the monsters have their lair treasure. |
Examples:
from osrlib.core.tables import ReactionResult
from osrlib.crawl.dungeon import KeyedEncounter, KeyedMonster
ambush = KeyedEncounter(
monsters=(KeyedMonster(template_id="goblin", count_fixed=6),),
aware=True,
stance=ReactionResult.ATTACKS,
)
print(ambush.aware, ambush.hoard)
# True True
monsters
class-attribute
instance-attribute
monsters: tuple[KeyedMonster, ...] = Field(min_length=1)
The monster lines making up the encounter, at least one. See
KeyedMonster.
alignment
class-attribute
instance-attribute
alignment: Alignment | None = None
The alignment the spawned monsters take, for a template whose own alignment offers more than
one. None rolls it the ordinary way. The value has to be one the template allows, and
validate_adventure refuses one that is not.
aware
class-attribute
instance-attribute
aware: bool = False
Whether the monsters already know the party is coming. Aware monsters never roll surprise, which is how you author a lookout or an ambush that has heard the party's armour.
stance
class-attribute
instance-attribute
stance: ReactionResult | None = None
The reaction the monsters take, instead of rolling for it. Set it when the room's monsters
attack on sight or are friendly by design. Leave it None and the reaction roll decides.
hoard
class-attribute
instance-attribute
hoard: bool = True
Whether the monsters have their lair treasure with them. The default generates their printed
hoard the first time the encounter spawns. Set it False for a monster room with no treasure,
which B/X stocking produces often: the room-contents roll puts treasure in only some monster
rooms, while a monster's printed lair letters would otherwise always come along.
KeyedMonster
Bases: BaseModel
One line of a keyed encounter: which monster, and how many.
A KeyedEncounter is a tuple of these, so a room holding
four orcs and their ogre bodyguard is two lines. Give each line a fixed count or count dice,
exactly one of the two.
Attributes:
| Name | Type | Description |
|---|---|---|
template_id |
str
|
Which monster stands here. |
count_dice |
str | None
|
How many, rolled when they spawn. |
count_fixed |
int | None
|
How many, decided now. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If both |
Examples:
from osrlib.crawl.dungeon import KeyedEncounter, KeyedMonster
guards = KeyedEncounter(monsters=(KeyedMonster(template_id="goblin", count_fixed=4),))
print(guards.monsters[0].template_id, guards.monsters[0].count_fixed)
# goblin 4
template_id
instance-attribute
template_id: str
The monster's template id. Any id the session's effective catalog holds works: a shipped id
from load_monsters, listed in
the monster id index, or one the adventure bundles on its monsters field.
count_dice
class-attribute
instance-attribute
count_dice: str | None = None
How many appear, as a dice expression like "2d4", rolled on the
WANDERING_STREAM the first time the party enters the
area, and held at 1 or more. Set this or count_fixed, not both. The expression is parsed when
the model is built, so a malformed one fails while you author rather than at play.
count_fixed
class-attribute
instance-attribute
count_fixed: int | None = None
How many appear, as a number decided now. Set this or count_dice, not both. A printed module
gives concrete numbers, and so does stock_area when it
rolls a room for you.
LevelSpec
Bases: BaseModel
One dungeon level: a grid of 10-foot cells with its edges, rooms, and stairs.
A level is where all of this module's geometry comes together. You give it a size, declare the
edges that are not wall, key some of its cells as areas, and hang features and transitions on it.
Then you put one or more levels in a DungeonSpec and that
dungeon in an Adventure.
The methods read the level back the way the engine does: is this cell on the grid, what stands on this side of it, which room is it part of, do stairs go from it. A front end drawing a map calls them, and so does a tool checking your work while you author.
Attributes:
| Name | Type | Description |
|---|---|---|
number |
int
|
The level's depth number. |
width |
int
|
The grid's width in cells. |
height |
int
|
The grid's height in cells. |
edges |
dict[str, Edge]
|
Everything that is not wall. |
areas |
tuple[AreaSpec, ...]
|
The keyed rooms. |
features |
tuple[FeatureSpec, ...]
|
Features on the level rather than on a room. |
transitions |
tuple[TransitionSpec, ...]
|
The ways to other levels. |
wandering |
WanderingSpec
|
The wandering-monster check. |
entrance |
Position | None
|
Where the party arrives from town. |
guidance |
str
|
Ambient steering for a narrating front end. |
Examples:
from osrlib.crawl.dungeon import Direction, Edge, EdgeKind, LevelSpec
# Two cells, joined west to east, entered at the west end.
corridor = LevelSpec(
number=1,
width=2,
height=1,
entrance=(0, 0),
edges={"1,0:west": Edge(kind=EdgeKind.OPEN)},
)
print(corridor.edge((0, 0), Direction.EAST).kind)
# open
print(corridor.edge((0, 0), Direction.NORTH).kind)
# wall
number
class-attribute
instance-attribute
The level's depth, 1-based and visible to the rules: it selects the wandering-monster table
band and the treasure bands. Level 1 is the top. The numbers have to be unique within a dungeon,
and a TransitionSpec names one to say where it goes.
width
class-attribute
instance-attribute
How many cells the grid runs east to west. Valid x values are 0 to width - 1.
height
class-attribute
instance-attribute
How many cells the grid runs north to south. Valid y values are 0 to height - 1.
edges
class-attribute
instance-attribute
areas
class-attribute
instance-attribute
The level's keyed rooms and caves. Cells no area covers are corridor. Ids have to be unique within the level, and every cell an area names has to be on the grid.
features
class-attribute
instance-attribute
features: tuple[FeatureSpec, ...] = ()
Features that belong to the level rather than to a room: a cache in a corridor, a trick in a
dead end. Each one needs a cell, since there is no area to bind it to. Ids share one namespace
with the areas' features.
transitions
class-attribute
instance-attribute
transitions: tuple[TransitionSpec, ...] = ()
The stairs, trapdoors, and chutes on this level, each standing on one cell. Transitions belong to the level, not to an area, even when they stand inside a room.
wandering
class-attribute
instance-attribute
wandering: WanderingSpec = WanderingSpec()
The level's wandering-monster check. The default is the printed rule, a 1-in-6 check every two turns off the compiled table for this level's number.
entrance
class-attribute
instance-attribute
entrance: Position | None = None
The cell the party arrives at from town, or None for a level with no way in from outside.
EnterDungeon puts the party here facing north, whatever
the geometry around the cell looks like, and
TravelToTown refuses to leave unless the party is
standing on it. Some level of every dungeon needs one, and validate_adventure refuses a dungeon
where no level has any.
guidance
class-attribute
instance-attribute
guidance: str = ''
Ambient steering for a narrating front end while the party is on this level: the tone of the place, what you want said about it, what you never want said.
The engine reads it nowhere, no event contains it, and no rule turns on it. It is authored data for a narrator, which reaches it through the adventure document. That document is referee-side, since the player view contains no level internals, so treat this the way you treat an area's description prose and don't show it to the player word for word.
in_bounds
Return whether a cell lies on this level's grid.
Everything off the grid is outside the dungeon, which the engine treats as solid: the party
cannot walk there, and an authored position out here is a content error that
validate_adventure catches.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
position
|
Position
|
The cell to test. |
required |
Returns:
| Type | Description |
|---|---|
bool
|
True when |
edge
Return what stands on one side of a cell.
This is the read you want rather than indexing edges yourself: it canonicalizes the key, so
a cell's east side and its neighbour's west side give the same answer, and it supplies the
wall for everything absent. Call it to find out whether the party can walk that way, whether
there is a door to open, and which door the overlay's state belongs to.
An edge with no entry in the map is wall, because authored content declares its passages rather than its walls, and the level boundary is wall too.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
position
|
Position
|
The cell. |
required |
direction
|
Direction
|
Which of the cell's four edges. |
required |
Returns:
| Type | Description |
|---|---|
Edge
|
The edge entry, or a wall edge when none is authored or either cell is off the grid. The
wall is a fresh |
area_at
Return the keyed area covering a cell, or None for corridor.
The engine calls this on every step to work out whether the party has just walked into a room and its content is due. Call it to label the party's location, or to show which room a cell belongs to on a map.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
position
|
Position
|
The cell. |
required |
Returns:
| Type | Description |
|---|---|
AreaSpec | None
|
The first area whose |
transition_at
transition_at(position: Position) -> TransitionSpec | None
Return the transition standing on a cell, or None.
UseStairs asks this and refuses when the answer is
None, which is also what makes a chute one-way: the arrival cell holds no transition back.
Call it to show a stairs marker on a map, or to offer the command only where it works.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
position
|
Position
|
The cell. |
required |
Returns:
| Type | Description |
|---|---|
TransitionSpec | None
|
The first transition authored on that cell, or |
PartyLocation
Bases: BaseModel
Where the party is: in the base town, or on a dungeon cell facing a direction.
The session keeps one of these on
DungeonState.location and moves it as the party moves. You
read it to draw the map and to know which mode the party is in, and you never write it. The referee
command PlaceParty is how a game moves the party by fiat.
The two shapes are exclusive and the model enforces it: a town location carries no dungeon fields, and a dungeon location carries all four.
Attributes:
| Name | Type | Description |
|---|---|---|
kind |
Literal['town', 'dungeon']
|
Which of the two shapes this is. |
dungeon_id |
str | None
|
The dungeon the party is in. |
level_number |
int | None
|
The level it is on. |
position |
Position | None
|
The cell it stands on. |
facing |
Direction | None
|
The direction it faces. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Examples:
from osrlib.crawl.dungeon import Direction, PartyLocation
here = PartyLocation(kind="dungeon", dungeon_id="crypt", level_number=1, position=(0, 0), facing=Direction.EAST)
print(here.position, here.facing)
# (0, 0) east
kind
instance-attribute
kind: Literal['town', 'dungeon']
"town" when the party is in the base town between delves, "dungeon" when it is standing on
a grid. A new session starts in town.
dungeon_id
class-attribute
instance-attribute
dungeon_id: str | None = None
The id of the dungeon the party is in, and None in town.
level_number
class-attribute
instance-attribute
level_number: int | None = None
The 1-based number of the level the party is on, and None in town.
position
class-attribute
instance-attribute
position: Position | None = None
The cell the party stands on, and None in town.
facing
class-attribute
instance-attribute
facing: Direction | None = None
The direction the party faces, and None in town. Facing is what a front end draws the view
from. Movement itself names its own direction, so the party can step any way it likes regardless
of which way it looks.
TransitionSpec
Bases: BaseModel
A way between levels standing on one cell: stairs, a trapdoor, or a chute.
Transitions live on the level rather than on an area, in
LevelSpec.transitions. They are how a multi-level dungeon
joins up, and how two dungeons join if you point one at the other's id. The party takes one with
UseStairs, which lands it at to_position facing
to_facing. validate_adventure checks that the
cell you leave from and the cell you arrive at are both on their grids.
Nothing pairs transitions up for you. A staircase the party can walk back up is two transitions,
one on each level, pointing at each other. Leave the return one out and you have a chute: a
one-way drop, which UseStairs refuses to climb back because the arrival cell holds no
transition.
Attributes:
| Name | Type | Description |
|---|---|---|
kind |
Literal['stairs_up', 'stairs_down', 'trapdoor', 'chute']
|
Which kind of connection this is. |
position |
Position
|
The cell it stands on. |
to_dungeon_id |
str
|
The dungeon the party arrives in. |
to_level_number |
int
|
The level the party arrives on. |
to_position |
Position
|
The cell the party arrives at. |
to_facing |
Direction
|
The direction the party faces on arrival. |
requires |
GateSpec | None
|
An authored condition the party must satisfy to take it. |
kind
instance-attribute
kind: Literal['stairs_up', 'stairs_down', 'trapdoor', 'chute']
What the party sees and uses: "stairs_up", "stairs_down", "trapdoor", or "chute". The
value is descriptive. The destination fields set where the party goes, and the
presence or absence of a return transition decides whether it can come back.
position
instance-attribute
position: Position
The cell on this level where the transition stands. The party has to be standing here for
UseStairs to do anything.
to_dungeon_id
instance-attribute
to_dungeon_id: str
The id of the dungeon the party arrives in. Naming this level's own dungeon is the ordinary case. Naming another dungeon of the same adventure joins the two.
to_level_number
class-attribute
instance-attribute
The 1-based number of the level the party arrives on.
to_position
instance-attribute
to_position: Position
The cell the party arrives at, on the destination level's grid.
to_facing
instance-attribute
to_facing: Direction
The direction the party faces on arrival, so a front end knows which way the view points and the party's first step is not a surprise.
requires
class-attribute
instance-attribute
requires: GateSpec | None = None
An authored gate on taking the transition, or None for one anyone may take.
UseStairs evaluates it after the there-is-no-transition-here
refusal and before the party moves, so a gate that costs the party something is paid at the
threshold. The gate's success narration is attached to the arrival's
LocationEnteredEvent, so a transition whose
destination is its own level crosses no boundary, emits no such event, and has nowhere to show
one. A transition inside a TrapEffect may carry no gate at
all: that is a forced relocation rather than an attempt, and the trap effect rejects one that
does.
TrapEffect
Bases: BaseModel
What a sprung trap does to its victim.
You attach one to a TrapSpec, which is what says when it
springs. The fields compose: a dart trap rolls damage, a pit trap rolls falling damage and may add
a condition, a gas trap calls for a save and kills on a failure. Leave everything unset and put
your description in manual for a trap your front end narrates and resolves itself.
A passed save always spares the victim from kills and from condition, whatever on_save says,
because half a death and half a blindness are not things B/X expresses. on_save scales damage
only.
Attributes:
| Name | Type | Description |
|---|---|---|
damage_dice |
str | None
|
The damage the trap deals. |
volley_dice |
str | None
|
The number of projectiles, for a trap that fires several. |
save |
SaveSpec | None
|
The saving throw the victim gets. |
kills |
bool
|
Whether a failed save kills outright. |
condition |
Condition | None
|
A condition a failed save inflicts. |
condition_duration_dice |
str | None
|
The condition's duration, rolled. |
condition_duration_amount |
int | None
|
The condition's duration, fixed. |
condition_duration_unit |
TimeUnit | None
|
The unit the duration counts in. |
fall_feet |
int | None
|
How far the victim falls. |
transition |
TransitionSpec | None
|
Where the trap drops the victim. |
manual |
str | None
|
Prose for a trap the rules do not resolve. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Examples:
from osrlib.core.combat import SaveCategory
from osrlib.core.spells import SaveSpec
from osrlib.crawl.dungeon import TrapEffect
# A dart trap: 1d6 darts, each for 1d4.
darts = TrapEffect(damage_dice="1d4", volley_dice="1d6", save=SaveSpec(category=SaveCategory.WANDS))
print(darts.kills)
# False
damage_dice
class-attribute
instance-attribute
damage_dice: str | None = None
The damage the trap deals, as a dice expression like "1d6", or None for a trap that deals
none. With volley_dice set this is the damage of one projectile rather than the whole trap. The
expression is parsed when the model is built, so a malformed one fails here rather than when the
trap springs.
volley_dice
class-attribute
instance-attribute
volley_dice: str | None = None
How many projectiles the trap fires, as a dice expression, or None for a trap that fires
one thing or none. This is the darts form: volley_dice="1d6" with damage_dice="1d4" fires
1d6 darts and rolls 1d4 for each. The dice grammar cannot say "this many times that much" on its
own, which is why it is two fields. It requires damage_dice.
save
class-attribute
instance-attribute
save: SaveSpec | None = None
The saving throw the victim rolls, or None for a trap that allows none. Its on_save says
what a successful save does, and negates spares the victim outright. A passed save always
spares them from kills and condition whatever on_save says. half halves damage, both the
damage_dice roll and fall_feet damage.
kills
class-attribute
instance-attribute
kills: bool = False
Whether a failed save kills the victim outright. This is the save-or-die form, poison gas
being the printed example. A passed save always spares them, whatever on_save says.
condition
class-attribute
instance-attribute
condition: Condition | None = None
A condition the trap inflicts on a failed save, or None. Blindness is the printed example.
A passed save always spares the victim from it.
condition_duration_dice
class-attribute
instance-attribute
condition_duration_dice: str | None = None
The condition's duration rolled as a dice expression, for a duration that varies. Set this or
condition_duration_amount, not both, and only alongside a condition. The roll draws on the
effects stream, like every other effect attachment.
condition_duration_amount
class-attribute
instance-attribute
condition_duration_amount: int | None = None
The condition's duration as a fixed number, for a duration that does not vary. Set this or
condition_duration_dice, and only alongside a condition.
condition_duration_unit
class-attribute
instance-attribute
condition_duration_unit: TimeUnit | None = None
What the duration counts in: rounds, turns, or days. Only meaningful alongside a condition.
A condition with no duration at all lasts until something removes it.
fall_feet
class-attribute
instance-attribute
fall_feet: int | None = None
How far the victim falls, in feet, or None for a trap with no drop. This is the pit form.
Falling damage is the SRD's own by distance, separate from damage_dice, and a half save
halves it too.
transition
class-attribute
instance-attribute
transition: TransitionSpec | None = None
Where the trap puts the victim, or None for a trap that moves nobody. This is the chute
form: the victim slides somewhere else instead of staying where they stood. The transition may
carry no requires gate, because the victim is not attempting anything.
manual
class-attribute
instance-attribute
manual: str | None = None
Prose for a trap the rules do not resolve, or None. Nothing here reads it: it is for the
referee or the front end, and it is how you author a trap whose effect is a judgement call rather
than a die roll.
TrapSpec
Bases: BaseModel
A trap: when it springs, what it does, and whom it catches.
There are two places a trap can sit, and the kind says which. A room trap goes on an
AreaSpec and covers the whole area. A treasure trap goes on a
FeatureSpec and guards that one cache. Each model accepts
only its own kind, so a trap cannot end up somewhere it has no meaning.
A trap does not spring on sight. The engine rolls a 2-in-6 chance each time the triggering action
happens, which is the SRD's rule, so walking into a trapped room is not certain death. A trap the
party has already found stops rolling: a room trap by a successful
Search, a treasure trap by a thief's
InspectTreasure. Only a treasure trap can then be taken
out of play, with RemoveTreasureTrap. A room trap the
party knows about is avoided rather than disarmed.
Traps you author are the only traps in the game. Treasure the engine generates is never trapped.
Attributes:
| Name | Type | Description |
|---|---|---|
kind |
Literal['room', 'treasure']
|
Whether this is a room trap or a treasure trap. |
trigger |
Literal['enter', 'open']
|
The action that springs it. |
effect |
TrapEffect
|
What it does when it springs. |
affects |
Literal['triggerer', 'party']
|
Whom it catches. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
kind
instance-attribute
kind: Literal['room', 'treasure']
"room" for a trap over an area, "treasure" for one on a cache.
AreaSpec accepts only room traps and
FeatureSpec only treasure traps.
trigger
instance-attribute
trigger: Literal['enter', 'open']
The action that springs the trap. "enter" is the party stepping into a cell of the trapped
area. "open" is a door being opened: for a room trap, any door of the area, from either side,
which is the blade that drops when the door swings. For a treasure trap, "open" is the cache
itself being opened. A treasure trap must use "open", because a cache has nothing to walk into.
affects
class-attribute
instance-attribute
affects: Literal['triggerer', 'party'] = 'triggerer'
Whom the effect lands on: "triggerer" for the one character who set it off, the default, or
"party" for every living member, which is the form for poison gas filling the room.
TreasureBundle
Bases: BaseModel
A working pile of rolled treasure: coins, valuables, and magic items together.
The treasure generators fill one of these while a hoard rolls, and the engine then moves its
contents into a GeneratedCache or a
DropPile. You meet it if you drive the generators yourself
outside a session. Inside one, the cache and the pile are what you read.
Unlike the authored models here it is mutable, because rolling a hoard adds to it entry by entry.
Attributes:
| Name | Type | Description |
|---|---|---|
coins |
Coins
|
The coins in the bundle. |
valuables |
list[ValuableInstance]
|
The gems and jewellery in the bundle. |
magic_items |
list[MagicItemInstance]
|
The magic items in the bundle. |
coins
class-attribute
instance-attribute
The coins, by denomination. A fresh bundle starts with none of each.
valuables
class-attribute
instance-attribute
valuables: list[ValuableInstance] = []
The gems and jewellery, each already an instance with its own id and value.
magic_items
class-attribute
instance-attribute
magic_items: list[MagicItemInstance] = []
The magic items, each already rolled out with its charges or quantity.
empty
property
empty: bool
Whether the bundle holds nothing at all.
A hoard can roll to nothing, and the engine checks this before it writes a cache, so an empty one never lands on the floor.
ValuableSpec
Bases: BaseModel
A named gem or piece of jewellery you placed by hand in a cache.
Use this when the treasure is a particular thing with a name, rather than one of the anonymous
gems the treasure generators roll. It goes in a
FeatureSpec's valuables. It stays a description until the
party empties the cache, at which point the session turns it into a
ValuableInstance with an id of its own.
Attributes:
| Name | Type | Description |
|---|---|---|
kind |
Literal['gem', 'jewellery']
|
Whether it is a gem or jewellery. |
name |
str
|
What the party sees it called. |
value_gp |
int
|
What it sells for, in gold pieces. |
weight_coins |
int
|
What it weighs, in coins. |
Examples:
from osrlib.crawl.dungeon import FeatureSpec, ValuableSpec
chest = FeatureSpec(
id="abbot_chest",
kind="treasure_cache",
valuables=(ValuableSpec(kind="jewellery", name="The abbot's seal ring", value_gp=900),),
)
print(chest.valuables[0].value_gp)
# 900
kind
instance-attribute
kind: Literal['gem', 'jewellery']
Which sort of valuable this is. Nothing mechanical turns on it. It is what the item is, for display and for any rule your game applies to one sort and not the other.
name
class-attribute
instance-attribute
name: str = ''
The display name the party sees, like "The abbot's seal ring". Empty leaves the valuable
unnamed, which is how the generated ones arrive.
value_gp
class-attribute
instance-attribute
What the valuable is worth in gold pieces. This is the sale price in town and the XP the party earns for bringing it back.
WanderingSpec
Bases: BaseModel
A level's wandering-monster check: how often it rolls, and from what.
Every LevelSpec has one, and the default is the B/X rule, so
you only write your own to change the odds or the monsters. The session runs the check on its own
clock as the party spends turns, and you never roll it yourself.
Attributes:
| Name | Type | Description |
|---|---|---|
chance_in_six |
int
|
The odds of monsters showing up. |
interval_turns |
int
|
How often the check runs. |
table |
EncounterTable | None
|
A custom monster table for this level. |
Examples:
from osrlib.crawl.dungeon import WanderingSpec
quiet = WanderingSpec(chance_in_six=0)
print(quiet.interval_turns)
# 2
chance_in_six
class-attribute
instance-attribute
How many faces of a d6 bring monsters, checked once per interval. The default of 1 is the printed rule. Set it 0 for a level nothing wanders on, and higher for one that is busier.
interval_turns
class-attribute
instance-attribute
How many exploration turns pass between checks. The default of 2 is the printed rule.
table
class-attribute
instance-attribute
table: EncounterTable | None = None
A custom encounter table for this level, or None to use the compiled table for the level's
number band. Setting it replaces the band table entirely, which is how you give a level its own
inhabitants. Same row model as the shipped tables, from
load_encounter_tables.
cell_ref
Return the reference string that names one cell across a whole adventure.
An edges key locates a cell inside one level. A cell reference locates it inside the game: it
carries the dungeon and the level too, which is what the state overlay and the effects system
need. Drop piles key on this in DungeonState.piles, and an
ActiveEffect.target_ref in this form anchors an effect to a
dungeon cell rather than to a creature.
The format is "cell:{dungeon}:{level}:{x},{y}".
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dungeon_id
|
str
|
The dungeon id, as it appears on its
|
required |
level_number
|
int
|
The 1-based level number. |
required |
position
|
Position
|
The cell. |
required |
Returns:
| Type | Description |
|---|---|
str
|
The cell reference string. |
Examples:
edge_key
Return the canonical key for the edge on direction's side of position.
Use this whenever you write a LevelSpec.edges map, so you
don't have to work out which of the two neighbouring cells owns the boundary between them. Every
physical edge has exactly one key: a cell plus north or west. A cell's south edge is its
southern neighbour's north edge, and its east edge is its eastern neighbour's west edge, so
edge_key((0, 0), Direction.EAST) and edge_key((1, 0), Direction.WEST) are the same string.
The format is "{x},{y}:{side}", which is what you see in a serialized adventure and what
LevelSpec.edge looks up for you at read time.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
position
|
Position
|
The cell. |
required |
direction
|
Direction
|
Which of the cell's four edges. |
required |
Returns:
| Type | Description |
|---|---|
str
|
The canonical edge key. It is not checked against any level, so a key for a cell off the grid comes back the same way. |
Examples:
edge_ref
Return the reference string that names one physical edge across a whole adventure.
This is what DungeonState.doors keys on, so it is how you
look a door's live open, wedged, discovered, and unlocked flags up from a cell and a direction.
It canonicalizes the same way edge_key does, so both sides of
a door produce one reference and the two sides can never disagree about its state. Pass the
result to DungeonState.door.
The format is "{dungeon}:{level}:{x},{y}:{side}".
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dungeon_id
|
str
|
The dungeon id. |
required |
level_number
|
int
|
The 1-based level number. |
required |
position
|
Position
|
The cell. |
required |
direction
|
Direction
|
Which of the cell's four edges. |
required |
Returns:
| Type | Description |
|---|---|
str
|
The edge reference string, canonicalized like |
Examples:
step
Return the cell one step from position in direction.
This is grid arithmetic, and it takes no account of walls, levels, or the party. Ask
LevelSpec.in_bounds whether the answer is on the
grid and LevelSpec.edge whether the party could get
there. Use it while authoring to walk a corridor cell by cell, or in a front end to work out
which cell a click landed on.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
position
|
Position
|
The starting cell. |
required |
direction
|
Direction
|
The direction to step. |
required |
Returns:
| Type | Description |
|---|---|
Position
|
The adjacent cell address, which may lie outside the level. |
Examples: