Skip to content

osrlib.crawl.stocking

SRD dungeon stocking: roll one keyed area's contents from the stocking tables.

The stocking tables ship as data already — the room-contents d6 with its per-row treasure-presence chance (StockingTable), the compiled level-band encounter tables (load_encounter_tables), and the treasure generators. This module is the procedure that consumes them: given a dungeon level, an effective monster catalog, and one RNG stream, stock_area rolls what a single keyed area holds and answers a frozen StockedArea — content models an author can review, place, and edit, never engine state.

Determinism is the whole point: every draw comes from the one passed RngStream, in a fixed order, so a stocked area is reproducible from (the stream's state, the level, the table) alone. The order, mirrored on the crawl's own wandering resolution so stocking a row yields precisely what a wandering encounter on that row would:

  1. the stocking d6 (room contents), then the treasure d6 — but only when the selected row's treasure_chance_in_six is non-zero (a printed None chance consumes no die);
  2. on a monster room, the encounter table's d20 row, then that row's count dice (a fixed count consumes none), clamped max(1, count);
  3. then either one variant_dice roll (the hydra form: the printed HD dice select the template once) or, for a packed-variant pool row, one uniform pick per individual.

The boundary is deliberate. A monster room's rolled individuals group by template into KeyedMonster lines whose count_fixed is set at stocking time — printed modules give concrete counts, and a concrete number is what an author reviews and edits. An empty or trap room that rolls treasure gets an unguarded AreaTreasureSpec; a monster room's treasure is the encounter itself (its hoard flag), never a second declaration. Traps and specials produce no models — B/X ships example lists as referee prose, not tables — and an NPC-party row has no authorable content at all: stock_area reports the rolled kind and count and stops. The procedure ends where the referee's design begins.

StockedArea

Bases: BaseModel

The whole answer of stocking one keyed area — content models, never state.

contents is the stocking d6's outcome and treasure_present the treasure d6's (always False when the row's printed chance is zero, e.g. a special). At most one authorable payload rides alongside: encounter for a monster room, npc_party for a monster room whose d20 row rolled an NPC party, or treasure (only ever the unguarded form) for an empty or trap room that rolled treasure. A monster room's treasure is its encounter's hoard flag, so treasure stays None there; traps and specials carry no model of their own — those are the referee's to design.

contents instance-attribute

contents: Literal['empty', 'monster', 'special', 'trap']

treasure_present instance-attribute

treasure_present: bool

encounter class-attribute instance-attribute

encounter: KeyedEncounter | None = None

npc_party class-attribute instance-attribute

npc_party: StockedNpcParty | None = None

treasure class-attribute instance-attribute

treasure: AreaTreasureSpec | None = None

StockedNpcParty

Bases: BaseModel

An NPC-party stocking roll: the rolled party kind and its count.

An NPC-party encounter-table row has no authorable content model — a party is generated at play from class treasure, not a keyed encounter and not treasure letters. The dice still spoke (the row, then its count), so stock_area reports what they said and leaves the party for the author to place by hand.

kind instance-attribute

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

count class-attribute instance-attribute

count: int = Field(ge=1)

stock_area

stock_area(
    level_number: int, *, catalog: MonsterCatalog, stream: RngStream, table: EncounterTable | None = None
) -> StockedArea

Roll one keyed area's contents from the SRD stocking tables.

Runs the room-contents d6 and, when the row calls for it, the treasure d6; on a monster room it then rolls the encounter table (the caller's table when set, else the level's compiled band). Every draw comes from stream in the fixed order documented on the module, so the result is reproducible from the stream's state alone.

Parameters:

Name Type Description Default
level_number int

The dungeon level being stocked — selects the compiled encounter band when no table override is given, and (via the treasure generators at play) the unguarded band.

required
catalog MonsterCatalog

The effective monster catalog (the shipped catalog composed with the adventure's bundled templates, the way GameSession.effective_monsters composes it). Each rolled monster id is resolved through it exactly as session.spawn would at play, so a stocked encounter references only monsters the catalog holds and an authored override table naming a bundled monster resolves.

required
stream RngStream

The RNG stream every draw advances — what makes the result reproducible.

required
table EncounterTable | None

An authored encounter-table override (the level's WanderingSpec.table), mirroring wandering_check's own resolution; None uses load_encounter_tables().for_level.

None

Returns:

Type Description
StockedArea

The stocked area.

Raises:

Type Description
ValueError

If the encounter table rolls a monster id the effective catalog does not hold — a malformed table, the same refusal session.spawn raises at play.

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(42, "stock:1/1/7"))
assert area.contents in ("empty", "monster", "special", "trap")