Skip to content

osrlib.crawl.stocking

Roll what one keyed room contains, from the B/X stocking tables.

B/X fills an unkeyed dungeon room with dice: the referee rolls a d6 for whether the room has a monster, a trap, something special, or nothing, and a second d6 for whether it has treasure. This module runs that procedure for you while you write an adventure. It isn't part of play. It doesn't read or change a session, and it returns content for you to place, not game state.

The members, in the order you use them:

  • stock_area takes a dungeon level number, a monster catalog, and an RngStream, and rolls one room.
  • StockedArea is what it returns. Its contents field gives the kind of room. At most one of its encounter, npc_party, and treasure is set. Copy an encounter (a KeyedEncounter) or a treasure (an AreaTreasureSpec) onto the AreaSpec for that room.
  • StockedNpcParty is the value of npc_party when the monster roll comes up as a party of NPC adventurers (characters the referee runs).

The tables come from the compiled data: the room-contents table is StockingTable, and the encounter tables come from load_encounter_tables.

Every draw comes from the stream you pass, in a fixed order, so the same stream state and the same level give the same room every time. The order is the one the session uses for a wandering encounter, so stocking a monster room gives exactly what a wandering encounter on the same table row would:

  1. The room-contents d6. Then the treasure d6, but only when the row it selected gives a chance of treasure. For a row with no chance, stock_area doesn't roll a second die.
  2. On a monster room, the d20 for the encounter table's row, then that row's count dice. For a row with a fixed count, it rolls no count dice. A count below 1 counts as 1.
  3. Then, for a row whose monster depends on a die roll, like a hydra whose number of heads is rolled, one roll that picks the monster for the whole group. For a row that lists several monsters, one pick for each individual, each monster equally likely.

stock_area groups a monster room's individuals into KeyedMonster lines with fixed counts, the way a printed adventure gives concrete numbers that you can review and edit. An empty or trap room with treasure gets an AreaTreasureSpec for unguarded treasure. A monster room's treasure is the encounter's hoard flag instead. A trap or a special room gets no model, because B/X describes those with example lists for the referee rather than with tables, and an NPC party gets only its kind and size. You design the traps, the specials, and the NPC parties yourself.

Typical usage:

from osrlib.core.rng import RngStream
from osrlib.crawl.dungeon import AreaSpec
from osrlib.crawl.stocking import stock_area
from osrlib.data import load_monsters

catalog = load_monsters()
stream = RngStream.from_seed_material(23, "stocking")
areas = []
for number in range(1, 4):
    stocked = stock_area(1, catalog=catalog, stream=stream)
    room = AreaSpec(id=f"room-{number}", cells=((number, 0),), encounter=stocked.encounter, treasure=stocked.treasure)
    areas.append(room)

print([(area.id, area.encounter is not None, area.treasure is not None) for area in areas])
# [('room-1', True, False), ('room-2', True, False), ('room-3', False, True)]
print([(line.template_id, line.count_fixed) for line in areas[0].encounter.monsters])
# [('skeleton', 7)]

StockedArea

Bases: BaseModel

The content the stocking rolls produced for one keyed room, for you to place on an area.

stock_area returns one. Read contents to find out what kind of room you rolled, then copy the payload that came with it onto the AreaSpec you're building for that room.

There is at most one payload:

  • encounter on a monster room.
  • npc_party on a monster room whose encounter roll produced an NPC party.
  • treasure on an empty or trap room that rolled treasure.

A monster room's treasure is its encounter's hoard flag, so treasure is always None on a monster room. A trap or a special room has no payload, because B/X leaves those to the referee.

Attributes:

Name Type Description
contents Literal['empty', 'monster', 'special', 'trap']

What the room-contents d6 rolled.

treasure_present bool

Whether the treasure d6 gave the room treasure.

encounter KeyedEncounter | None

The monsters, on a monster room.

npc_party StockedNpcParty | None

The NPC party, when the encounter roll produced 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']

What the room-contents d6 rolled: "empty", "monster", "special", or "trap". A special is a magical or unusual feature, and a trap is a room trap. B/X describes both with example lists for the referee, so neither comes with a model.

treasure_present instance-attribute

treasure_present: bool

Whether the treasure d6 rolled within the row's chance of treasure. It's always False when the row gives no chance, as for a special room, and then stock_area rolls no die. On a monster room it's 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. Each line has a fixed count that is already rolled. The encounter's hoard is the same as treasure_present. It's also None on a monster room whose encounter roll produced an NPC party.

npc_party class-attribute instance-attribute

npc_party: StockedNpcParty | None = None

The NPC party the encounter roll produced, or None. It's set only on a monster room, and never together with 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's always the unguarded form, never treasure type letters, because a treasure type is a monster's hoard and no monster lives here. The session rolls the treasure the first time the party enters the area.

StockedNpcParty

Bases: BaseModel

The kind and size of NPC party that a monster room's encounter roll produced.

An NPC party is a band of adventurers the referee runs. No content model describes one, because the session builds the party during play from character classes and gear, not from monster templates and treasure letters. So stock_area has no model to hand back. It reports the kind and the count the roll gave instead.

You get one in StockedArea.npc_party, only on a room whose contents is "monster". To put a party in play, execute SpawnNpcParty with this kind as its party_kind, from your own code or as a trigger's consequence.

Attributes:

Name Type Description
kind Literal['basic', 'expert']

Which NPC party table the roll selected.

count int

How many adventurers are in the party.

kind instance-attribute

kind: Literal['basic', 'expert']

Which NPC party table the encounter row selected: "basic" for a party of low-level adventurers, or "expert" for a higher-level one.

count class-attribute instance-attribute

count: int = Field(ge=1)

How many adventurers are in the party, from the row's count. A count below 1 counts as 1.

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 it once for each room you want the dice to fill while you write an adventure. It rolls the room-contents d6 and, when the row it selects gives a chance of treasure, the treasure d6. On a monster room it then rolls on an encounter table: table when you pass one, and the compiled table for level_number otherwise. Copy the payload of the StockedArea it returns onto the AreaSpec you're building, then check the finished adventure with validate_adventure.

Every draw comes from stream, in the order the osrlib.crawl.stocking module describes, so the same stream state and level always give the same room. To stock a whole level, pass the same stream to each call. It advances from one room to the next.

Don't call it during play. It writes nothing, reads no session, and spawns nothing. The wandering-monster check during play is wandering_check, which rolls on the same tables.

Parameters:

Name Type Description Default
level_number int

The dungeon level the room is on, 1 or more. Without a table, it selects the compiled encounter table for that level. It doesn't affect unguarded treasure here: the session rolls that during play, from the number of the level the area is on.

required
catalog MonsterCatalog

The monster catalog to look up each rolled monster id in. Pass load_monsters() when the adventure bundles no monsters. When it does, pass the built-in catalog combined with the bundled templates, which is what GameSession.effective_monsters returns for a session. Because stock_area looks up every id, a stocked encounter refers only to monsters that exist.

required
stream RngStream

The RngStream that every draw advances. It's the only source of randomness in the call.

required
table EncounterTable | None

An encounter table to roll on instead of the compiled one for the level, usually a level's WanderingSpec.table. The session's wandering check rolls on that table too, so passing it stocks the room with the monsters you chose for the level. None, the default, uses load_encounter_tables for the level.

None

Returns:

Type Description
StockedArea

What the room contains. See StockedArea.

Raises:

Type Description
ValueError

If the encounter roll gives a monster id that catalog doesn't contain. Pass a catalog that includes the adventure's bundled monsters, or fix the table. Spawning raises the same error during play.

Examples:

from osrlib.core.rng import RngStream
from osrlib.crawl.dungeon import AreaSpec
from osrlib.crawl.stocking import stock_area
from osrlib.data import load_monsters

stocked = stock_area(1, catalog=load_monsters(), stream=RngStream.from_seed_material(7, "stocking"))
print(stocked.contents, stocked.treasure_present)
# monster False

guard_room = AreaSpec(id="guard-room", cells=((2, 0),), encounter=stocked.encounter, treasure=stocked.treasure)
print([(line.template_id, line.count_fixed) for line in guard_room.encounter.monsters])
# [('gecko', 3)]