osrlib.crawl.stocking
Stocking: roll what one keyed room holds, from the B/X tables.
stock_area is the entry point, and the two models here are what
it returns. Give it a dungeon level number,
a monster catalog, and an RNG stream, and it rolls one room the way the B/X procedure does: the
room-contents d6, the treasure chance that row prints, and, on a monster room, the level's encounter
table and the count that row calls for. What comes back is a frozen
StockedArea holding content models you can read, edit, and
place: a KeyedEncounter to drop on an
AreaSpec, or an
AreaTreasureSpec to put on one. It is an authoring tool,
not part of play: nothing here touches a session, and a stocked area is content rather than state.
The tables it rolls on ship as data already. The room-contents d6 with its per-row treasure chance is
StockingTable, the encounter tables are
load_encounter_tables, and the treasure generators live in
osrlib.core.treasure. This module is the procedure that puts them together.
Every draw comes from the one stream you pass, in a fixed order, so the same stream state and the same level give the same room every time. The order matches the crawl's own wandering resolution, so stocking a row yields exactly what a wandering encounter on that row would:
- the room-contents d6, then the treasure d6, the second only when the selected row prints a non-zero treasure chance (a row with no printed chance consumes no die).
- on a monster room, the encounter table's d20 row, then that row's count dice (a row with a fixed count consumes none), with the result held at 1 or more.
- then either one
variant_diceroll, which is the hydra form where the printed hit-dice roll selects the template once, or, for a packed-variant pool row, one uniform pick per individual.
Where the procedure stops is deliberate. A monster room's rolled individuals are grouped into
KeyedMonster lines with fixed counts, because a printed module
gives concrete numbers and a concrete number is what you review and edit. An empty or trap room that
rolled treasure gets an unguarded AreaTreasureSpec. A monster room's treasure is the encounter's
own hoard flag rather than a second declaration. Traps and specials produce no model at all, because
B/X prints example lists for those as referee prose rather than as tables, and an NPC-party row has no
authorable content either, so stock_area reports the kind and the count and stops. The procedure
ends where the dice end, and the rest is yours to design.
Typical usage:
from osrlib.core.rng import RngStream
from osrlib.crawl.stocking import stock_area
from osrlib.data import load_monsters
catalog = load_monsters()
stream = RngStream.from_seed_material(7, "stocking")
for _ in range(3):
area = stock_area(1, catalog=catalog, stream=stream)
lines = [(line.template_id, line.count_fixed) for line in (area.encounter.monsters if area.encounter else ())]
print(area.contents, area.treasure_present, lines)
# monster False [('gecko', 3)]
# monster False [('trader', 3)]
# special False []
StockedArea
Bases: BaseModel
Everything the rolls produced for one keyed room: content models you place, never game state.
This is what stock_area returns. Read contents first to
find out what sort of room you rolled, then take whichever payload came with it and write it onto
an AreaSpec you are building.
At most one payload rides along: encounter for a monster room, npc_party for a monster room
whose encounter row turned out to be an NPC party, or treasure for an empty or trap room that
rolled treasure. A monster room's treasure is its encounter's hoard flag rather than a separate
declaration, so treasure is always None there. Traps and specials have no payload at all,
because B/X leaves those to the referee.
Attributes:
| Name | Type | Description |
|---|---|---|
contents |
Literal['empty', 'monster', 'special', 'trap']
|
The room-contents d6's result. |
treasure_present |
bool
|
The treasure d6's result. |
encounter |
KeyedEncounter | None
|
The monsters, on a monster room. |
npc_party |
StockedNpcParty | None
|
The NPC party, when the encounter row rolled one. |
treasure |
AreaTreasureSpec | None
|
The unguarded treasure, on an empty or trap room that rolled some. |
contents
instance-attribute
contents: Literal['empty', 'monster', 'special', 'trap']
The room-contents d6's result: "empty", "monster", "special", or "trap". A special is
a magical or unusual feature and a trap is a room trap, and B/X prints example lists for both as
referee prose, so neither comes with a model.
treasure_present
instance-attribute
treasure_present: bool
Whether the treasure d6 came up in the room's favour. It is always False when the row prints
no treasure chance, which is the case for a special, and no die is rolled then. On a monster room
this is the same value as the encounter's hoard flag.
encounter
class-attribute
instance-attribute
encounter: KeyedEncounter | None = None
The monsters in the room, on a monster room, and None otherwise. The lines carry fixed
counts, already rolled, and the encounter's hoard follows treasure_present. It is None on a
monster room whose row rolled an NPC party.
npc_party
class-attribute
instance-attribute
npc_party: StockedNpcParty | None = None
The NPC party the encounter row rolled, or None. Only ever set on a monster room, and never
alongside encounter. See StockedNpcParty.
treasure
class-attribute
instance-attribute
treasure: AreaTreasureSpec | None = None
Unguarded treasure for an empty or trap room that rolled some, and None otherwise. It is
always the unguarded form rather than named letters, since nothing is lairing here to have a
printed hoard.
StockedNpcParty
Bases: BaseModel
The party kind and size a monster room's encounter row rolled.
An NPC party has no authorable content model. A party is built at play from character classes and
their gear rather than from a keyed encounter or treasure letters, so there is nothing for
stock_area to hand back and place. The roll still named a
row and a count, so it reports that much and leaves the party for you to write by hand.
You get one on StockedArea.npc_party, and only on a room
whose contents is "monster".
Attributes:
| Name | Type | Description |
|---|---|---|
kind |
Literal['basic', 'expert']
|
Which encounter list the party came off. |
count |
int
|
How many are in it. |
kind
instance-attribute
kind: Literal['basic', 'expert']
Which of the two NPC-party lists the row came off: "basic" or "expert". The two lists
describe different sorts of party, and the row you rolled names one of the two.
stock_area
stock_area(
level_number: int, *, catalog: MonsterCatalog, stream: RngStream, table: EncounterTable | None = None
) -> StockedArea
Roll one keyed room's contents from the B/X stocking tables.
Call this once per room you want the dice to fill, while you are authoring. It rolls the
room-contents d6 and, when that row prints a treasure chance, the treasure d6. On a monster room
it then rolls the encounter table, using table when you pass one and the level's compiled band
otherwise. Take the StockedArea it returns and write its
payload onto an AreaSpec you are building, then check the
finished adventure with
validate_adventure.
Every draw comes from stream in the fixed order osrlib.crawl.stocking
sets out, so the result is reproducible from the stream's state and the level alone. Pass the same
stream to successive calls to stock a whole level, and it advances between rooms.
This is not something you call during play. It writes nothing, reads no session, and rolls
content models rather than spawning anything. The session's own wandering check is
wandering_check, and it works from the same tables.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
level_number
|
int
|
The dungeon level being stocked. It selects the compiled encounter band when you
pass no |
required |
catalog
|
MonsterCatalog
|
The monster catalog to resolve rolled ids against. Pass the session's effective
catalog, which is the shipped one composed with the adventure's bundled templates the way
|
required |
stream
|
RngStream
|
The |
required |
table
|
EncounterTable | None
|
An encounter table to roll on instead of the level's compiled band, usually a level's
own |
None
|
Returns:
| Type | Description |
|---|---|
StockedArea
|
What the room holds. See |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the encounter table rolls a monster id |
Examples:
from osrlib.core.rng import RngStream
from osrlib.crawl.stocking import stock_area
from osrlib.data import load_monsters
area = stock_area(1, catalog=load_monsters(), stream=RngStream.from_seed_material(7, "stocking"))
print(area.contents, area.treasure_present)
# monster False
print([(line.template_id, line.count_fixed) for line in area.encounter.monsters])
# [('gecko', 3)]