Skip to content

osrlib.core.npc

NPC adventuring parties: a rival band of adventurers, rolled up from the SRD's procedure.

generate_npc_party is the entry point. Tell it how many members and whether they are the Basic or the Expert kind, hand it two seeded streams and an IdAllocator, and you get an NpcParty: a band of classed characters with gear, memorized spells, and treasure to take off them. Use it when a wandering monster roll turns up other adventurers, or when you want a rival party for an encounter you are writing.

Its members are ordinary Character models, the same ones the players use, so everything else in the library takes them as they are: they fight through osrlib.core.combat, cast through osrlib.core.spells, and can be put into a Party if you want to run them as one. npc_defeat_xp gives the experience a party earns for defeating one of them.

How many adventurers appear is not decided here. Roll the count from the wandering monster table that produced the encounter, then pass it in.

Every member's own draws come from the NPC_PARTY_STREAM stream: the class and level, the ability scores, the hit points, the spells they have prepared. The party's treasure and the Expert band's magic items come from the TREASURE_STREAM stream instead, because they are treasure rolls and belong with the rest of a game's treasure statistics.

Four things here are osrlib's reading rather than the SRD's letter, and all four appear in the adaptations register, the site page that collects the places where osrlib settles an ambiguous rule one way or supplies a default the tabletop game leaves to a referee. NPC adventurers are not checked against their class's ability requirements, because the SRD's procedure rolls the class before the scores and offers no re-roll. The equipment kits are osrlib's, standing in for the SRD's "normal adventuring gear". Casters get spells rolled at random from the ones their class may cast, since the SRD lets the referee choose or roll and only rolling is repeatable. An Expert band's magic items are rolled at 5% per level against each kind of item the member could use, and an item nobody can use is dropped rather than re-rolled.

Typical usage:

from osrlib.core.monsters import IdAllocator
from osrlib.core.npc import NPC_PARTY_STREAM, generate_npc_party
from osrlib.core.rng import RngStreams
from osrlib.core.treasure import TREASURE_STREAM

streams = RngStreams(master_seed=5)
party = generate_npc_party(
    "basic",
    count=3,
    npc_stream=streams.get(NPC_PARTY_STREAM),
    treasure_stream=streams.get(TREASURE_STREAM),
    allocator=IdAllocator(),
)
print(party.alignment.value)
# neutral
for member in party.members:
    print(member.id, member.class_id, member.level, member.max_hp)
# npc-0001 halfling 2 4
# npc-0002 thief 3 14
# npc-0003 fighter 1 6

NPC_PARTY_STREAM module-attribute

NPC_PARTY_STREAM = StreamName.NPC_PARTY

The stream key every session uses for rolling up NPC adventurers.

A stream key names one independent random-number sequence inside an RngStreams set. Pass streams.get(NPC_PARTY_STREAM) as the npc_stream argument of generate_npc_party, which draws the party's alignment and then each member's class, level, ability scores, hit points, and spells from it.

The party's treasure and its magic items do not come from this stream. They are treasure rolls, and they draw from TREASURE_STREAM so that a change to how NPC parties are built does not shift the treasure a game has already recorded.

NpcParty

Bases: BaseModel

A band of NPC adventurers: who they are, what they believe, and what they carry between them.

Returned by generate_npc_party. Run them as an encounter: roll reaction with osrlib.crawl.encounter, fight them through osrlib.core.combat, and award npc_defeat_xp per member if the players win.

kind instance-attribute

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

"basic" for a band of low-level adventurers or "expert" for a seasoned one. It decided the level dice, the armour the members wear, and whether they carry magic items.

alignment instance-attribute

alignment: Alignment

The alignment the whole band shares. One roll covers everyone, so reactions, parleys, and the wards that turn on alignment all have a single answer.

members instance-attribute

members: list[Character]

The adventurers, as ordinary Character models with ids from the allocator you passed. Everything in the library that takes a character takes these.

treasure instance-attribute

What the band carries between them, rolled once for the group rather than per member. In a crawl the band carries it as one bundle, which drops on the party's cell as a pile once the whole band is slain. A band with a member who ran takes it with them.

generate_npc_party

generate_npc_party(
    kind: Literal["basic", "expert"], *, count: int, npc_stream: RngStream, treasure_stream: RngStream, allocator: Any
) -> NpcParty

Roll up a band of NPC adventurers, complete with gear, spells, and treasure.

Use it when your game needs other adventurers: a wandering encounter, a rival party in a keyed room, a patrol. You supply the size, because the table that produced the encounter sets how many appear. Everything else is rolled here.

The band shares one alignment, rolled once, so their reaction to the players and their vulnerability to alignment-gated wards have a single answer. Then each member in turn gets a class and a level from the SRD's table, ability scores rolled 3d6 in order, hit points, experience set to the threshold for their level, an equipment kit their class can use, and, if they cast, spells prepared at random from their class's list. An Expert band wears heavier armour and each member gets a 5% chance per level at each kind of magic item they could use.

Hit points come in two parts, which matters if you are counting draws. The first level's hit die is rolled here, directly, with the CON modifier added and the total floored at 1. Every level after the first goes through level_up, one call per level, and each of those calls takes a draw only when that level's row adds a hit die. An Expert dwarf rolled at level 11 or 12 passes name level, so its top levels take no draw.

Members are not checked against their class's ability requirements, because the SRD rolls their class before their scores. An elf here may have an INT a player character would not be allowed.

The draws come off the two streams in a fixed order, which is what makes a seeded encounter repeatable: the alignment and every member's own rolls from npc_stream in member order, then each member's magic items and finally the shared treasure from treasure_stream.

Parameters:

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

"basic" for a band of levels 1 to 3, or "expert" for a seasoned one whose level dice depend on the class rolled.

required
count int

How many adventurers appear. Roll it from the encounter table that sent them.

required
npc_stream RngStream

The stream for the members themselves, conventionally streams.get(NPC_PARTY_STREAM).

required
treasure_stream RngStream

The stream for their magic items and their shared treasure, conventionally streams.get(TREASURE_STREAM), so those rolls land in the treasure statistics with every other treasure roll.

required
allocator Any

The IdAllocator that names the members and the items and valuables they carry. Pass the session's own allocator so nothing collides with ids already in play.

required

Returns:

Type Description
NpcParty

The band, its shared alignment, and its treasure.

Examples:

from osrlib.core.monsters import IdAllocator
from osrlib.core.npc import NPC_PARTY_STREAM, generate_npc_party, npc_defeat_xp
from osrlib.core.rng import RngStreams
from osrlib.core.treasure import TREASURE_STREAM

streams = RngStreams(master_seed=5)
party = generate_npc_party(
    "basic",
    count=2,
    npc_stream=streams.get(NPC_PARTY_STREAM),
    treasure_stream=streams.get(TREASURE_STREAM),
    allocator=IdAllocator(),
)
print(party.kind, party.alignment.value)
# basic neutral
for member in party.members:
    print(member.name, member.level, member.max_hp, member.armour_class)
# Halfling adventurer 1 2 4 6
# Thief adventurer 2 3 14 7
print(sum(npc_defeat_xp(member.level) for member in party.members))
# 55

npc_defeat_xp

npc_defeat_xp(level: int) -> int

Return the experience a party earns for defeating one NPC adventurer of this level.

Call it once per defeated member of an NpcParty, add the results together with whatever else the party overcame, and hand the total to apply_xp for each surviving character.

The SRD prices monsters by Hit Dice and says nothing about classed NPCs, so osrlib prices an NPC adventurer as a monster of as many Hit Dice as they have levels, with no bonus for special abilities. The adaptations register, the site page that collects the places where osrlib settles an ambiguous rule one way, records the reading.

Parameters:

Name Type Description Default
level int

The NPC's class level.

required

Returns:

Type Description
int

The experience for defeating them.

Examples:

from osrlib.core.npc import npc_defeat_xp

print(npc_defeat_xp(1), npc_defeat_xp(3), npc_defeat_xp(5))
# 10 35 175