Skip to content

osrlib.core.tables

The rules tables as data: attack matrix, saves, XP, turning, reaction, encounters.

The combat tables — the attack matrix, monster saving throws, XP awards, turning undead, and monster reactions — ship with the package as frozen models and load via load_combat_tables. The dungeon encounter tables (six dungeon-level columns plus the NPC adventurer generation tables) load via load_encounter_tables. Load first, then resolve with the helpers here: to_hit_ac, reaction_result, monster_xp, and their kin.

The shipped attack matrix matches the OSE SRD verbatim, and every printed cell equals clamp(THAC0 − AC, 2, 20); AC values outside the printed −3..9 columns extend by the same formula — the printed bounds are page layout, not a rules cliff. The clamping is exactly what distinguishes matrix mode from the thac0_arithmetic ruleset flag once modifiers push totals past the plateaus.

Monster stat blocks carry explicit THAC0 and save values (already reflecting the "bonus hit points attack as 1 HD higher" rule), so the HD-keyed lookups here serve validation, custom monsters, and the save-as resolutions from packed-variant expansion.

TURNING_COLUMNS module-attribute

TURNING_COLUMNS = ('1', '2', '2*', '3', '4', '5', '6', '7-9')

The turning table's monster-HD column labels, exactly as the SRD's cleric turning table prints them.

EncounterEntry module-attribute

EncounterEntry = Annotated[MonsterEncounterEntry | NpcPartyEncounterEntry, Field(discriminator='kind')]

Any encounter-table entry, discriminated by kind.

AttackMatrix

Bases: BaseModel

The attack matrix: 16 THAC0 rows from 20 [-1]/NH to 5 [+14].

rows instance-attribute

rows: tuple[AttackMatrixRow, ...]

AttackMatrixRow

Bases: BaseModel

One attack matrix row: the monster-HD label, THAC0 both ways, and the cells.

by_ac maps AC −3..9 to the attack roll required to hit it, exactly as printed.

hd_label instance-attribute

hd_label: str

thac0 class-attribute instance-attribute

thac0: int = Field(ge=2, le=20)

attack_bonus class-attribute instance-attribute

attack_bonus: int = Field(ge=-1)

by_ac instance-attribute

by_ac: dict[int, int]

CombatTables

Bases: BaseModel

The loaded combat tables.

attack_matrix instance-attribute

attack_matrix: AttackMatrix

monster_saves instance-attribute

monster_saves: tuple[MonsterSaveBand, ...]

xp_awards instance-attribute

xp_awards: tuple[XpAwardRow, ...]

turning instance-attribute

turning: TurningTable

reaction instance-attribute

reaction: ReactionTable

save_band

save_band(label: str) -> MonsterSaveBand

Return the monster save band with label.

Parameters:

Name Type Description Default
label str

The band label, e.g. "NH" or "4–6".

required

Returns:

Type Description
MonsterSaveBand

The band.

Raises:

Type Description
ValueError

If no band has that label.

xp_row

xp_row(label: str) -> XpAwardRow

Return the XP-awards row with label.

Parameters:

Name Type Description Default
label str

The row label, e.g. "2+" or "7–7+".

required

Returns:

Type Description
XpAwardRow

The row.

Raises:

Type Description
ValueError

If no row has that label.

EncounterTable

Bases: BaseModel

One dungeon level column: twenty d20 rows.

min_level/max_level bound the printed band (max_level=None is the open 8+ column).

id instance-attribute

id: str

label instance-attribute

label: str

min_level class-attribute instance-attribute

min_level: int = Field(ge=1)

max_level class-attribute instance-attribute

max_level: int | None = None

rows instance-attribute

overrides_applied class-attribute instance-attribute

overrides_applied: tuple[str, ...] = ()

EncounterTableRow

Bases: BaseModel

One d20 row of a dungeon encounter table.

name is the printed cell name (after any documented data overrides). The count is the table's own parenthesized value and, per the SRD's note, overrides the monster description's number-appearing: count_dice carries dice forms and count_fixed the printed plain 1, exactly one of the two.

roll class-attribute instance-attribute

roll: int = Field(ge=1, le=20)

name class-attribute instance-attribute

name: str = Field(min_length=1)

entry instance-attribute

count_dice class-attribute instance-attribute

count_dice: str | None = None

count_fixed class-attribute instance-attribute

count_fixed: int | None = None

EncounterTables

Bases: BaseModel

The six dungeon encounter tables plus the NPC adventurer generation tables.

tables instance-attribute

tables: tuple[EncounterTable, ...]

npc_class_levels class-attribute instance-attribute

npc_class_levels: tuple[NpcClassLevelRow, ...] = ()

npc_alignment class-attribute instance-attribute

npc_alignment: tuple[NpcAlignmentBand, ...] = ()

npc_compositions class-attribute instance-attribute

npc_compositions: tuple[NpcPartyComposition, ...] = ()

for_level

for_level(level: int) -> EncounterTable

Return the table for a dungeon level, clamped into the printed bands.

A dungeon level's table is its level number clamped into the printed bands — 1, 2, 3, 4–5, 6–7, and everything 8 or deeper on 8+.

Parameters:

Name Type Description Default
level int

The dungeon level number, 1-based.

required

Returns:

Type Description
EncounterTable

The table for that level.

Raises:

Type Description
ValueError

If level is below 1.

MonsterEncounterEntry

Bases: BaseModel

An encounter-table cell resolving to monster template ids.

monster_ids are monster ids from load_monsters — see the monster id index. A single id is the common case. Multiple ids are a packed-variant pool (Veteran over veteran_1..3): the tabletop table leaves the pick to the referee, so osrlib has each spawned individual pick uniformly from the pool on the wandering stream — deterministic, and matching the printed spreads on the monster pages. variant_dice marks the hydra form: the printed HD dice roll once on the wandering stream and the total selects the template — monster_ids are ordered so the dice minimum maps to index 0.

kind class-attribute instance-attribute

kind: Literal['monster'] = 'monster'

monster_ids class-attribute instance-attribute

monster_ids: tuple[str, ...] = Field(min_length=1)

variant_dice class-attribute instance-attribute

variant_dice: str | None = None

MonsterSaveBand

Bases: BaseModel

One monster saving-throw band.

min_hd/max_hd bound the band's Hit Dice; NH is min_hd=None (normal humans are "less than 1 Hit Die" with their own row) and 22+ is max_hd=None.

label instance-attribute

label: str

min_hd class-attribute instance-attribute

min_hd: int | None = None

max_hd class-attribute instance-attribute

max_hd: int | None = None

saves instance-attribute

saves: SavingThrows

NpcAlignmentBand

Bases: BaseModel

One d6 band of the NPC Adventurer Alignment table.

roll_min class-attribute instance-attribute

roll_min: int = Field(ge=1, le=6)

roll_max class-attribute instance-attribute

roll_max: int = Field(ge=1, le=6)

alignment instance-attribute

alignment: str

NpcClassLevelRow

Bases: BaseModel

One d8 row of the NPC Adventurer Class and Level table.

Rows are keyed by die result — results 4 and 5 are both Fighter with different Expert level dice, per the survey.

roll class-attribute instance-attribute

roll: int = Field(ge=1, le=8)

class_id instance-attribute

class_id: str

basic_dice instance-attribute

basic_dice: str

expert_dice instance-attribute

expert_dice: str

NpcPartyComposition

Bases: BaseModel

One party kind's printed composition dice (Basic 1d4+4, Expert 1d6+3).

kind instance-attribute

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

count_dice instance-attribute

count_dice: str

NpcPartyEncounterEntry

Bases: BaseModel

An NPC adventuring party cell (Basic/Expert Adventurers).

The NPC generator builds the parties these rows call for; see osrlib.core.npc.

kind class-attribute instance-attribute

kind: Literal['npc_party'] = 'npc_party'

party_kind instance-attribute

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

ReactionBand

Bases: BaseModel

One reaction-table band: the printed 2d6 range and its result.

min_total is None for the open "2 or less" band and max_total is None for "12 or more" — the table's own clamping semantics: totals outside the printed 2..12 span land in the outer bands.

label instance-attribute

label: str

text instance-attribute

text: str

min_total class-attribute instance-attribute

min_total: int | None = None

max_total class-attribute instance-attribute

max_total: int | None = None

result instance-attribute

ReactionResult

Bases: StrEnum

The five monster reaction bands, from the OSE SRD's encounter rules.

The wire values are lowercase — they serialize into events and saves; changing them is a schema_version bump.

ATTACKS class-attribute instance-attribute

ATTACKS = 'attacks'

HOSTILE class-attribute instance-attribute

HOSTILE = 'hostile'

UNCERTAIN class-attribute instance-attribute

UNCERTAIN = 'uncertain'

INDIFFERENT class-attribute instance-attribute

INDIFFERENT = 'indifferent'

FRIENDLY class-attribute instance-attribute

FRIENDLY = 'friendly'

ReactionTable

Bases: BaseModel

The monster reaction table: five contiguous 2d6 bands.

bands instance-attribute

bands: tuple[ReactionBand, ...]

TurningResult

Bases: BaseModel

One turning-table cell, resolved.

outcome follows the table legend: fail (—), number (succeeds when the 2d6 turn roll meets threshold), turn (T — automatic), destroy (D — automatic, and affected monsters are annihilated rather than turned).

outcome instance-attribute

outcome: str

threshold class-attribute instance-attribute

threshold: int | None = None

TurningRow

Bases: BaseModel

One cleric level's turning row: cells keyed by the monster-HD column label.

Cell values are exactly as printed: , a threshold number, T, or D.

label instance-attribute

label: str

cells instance-attribute

cells: dict[str, str]

TurningTable

Bases: BaseModel

The turning-undead table: 11 cleric-level rows (1–10, 11+) × 8 HD columns.

rows instance-attribute

rows: tuple[TurningRow, ...]

result

result(cleric_level: int, column: str) -> TurningResult

Resolve the turning cell for a cleric level and monster-HD column.

Levels above 10 clamp to the 11+ row (the printed table's own semantics).

Parameters:

Name Type Description Default
cleric_level int

The turning cleric's level, 1 or higher.

required
column str

The monster-HD column label, from turning_column.

required

Returns:

Type Description
TurningResult

The resolved cell.

Raises:

Type Description
ValueError

If the level is below 1 or the column label is unknown.

XpAwardRow

Bases: BaseModel

One XP-awards row: the printed HD label, base XP, and bonus XP per ability.

label instance-attribute

label: str

base class-attribute instance-attribute

base: int = Field(ge=0)

bonus class-attribute instance-attribute

bonus: int = Field(ge=0)

monster_save_band_label

monster_save_band_label(hit_dice: MonsterHitDice) -> str

Return the monster saving-throw band label for a monster's Hit Dice.

Bonus hit-point modifiers round the effective HD up (a 6+3 troll saves as 6 — the printed bands are whole numbers and the SRD's stat blocks agree); fractional and fixed-hp forms save as NH... except that the SRD prints per-block save-as notes, which ship on every stat block — this lookup serves custom monsters and expansion resolutions.

Parameters:

Name Type Description Default
hit_dice MonsterHitDice

The monster's Hit Dice.

required

Returns:

Type Description
str

The band label: "NH", "1–3", ... "22 or more".

monster_xp

monster_xp(tables: CombatTables, hit_dice: MonsterHitDice) -> int

Return the XP award for defeating a monster with the given Hit Dice.

Base XP by HD row plus the asterisk count times the bonus column. Above 21 HD, both the base and the bonus amounts first gain 250 per HD above 21 ("add 250 XP to the Base and Bonus amounts") — the dragon turtle (HD 30*, XP 9,000) proves the reading: (2,500 + 9×250) + 1 × (2,000 + 9×250) = 9,000.

Parameters:

Name Type Description Default
tables CombatTables

The loaded combat tables.

required
hit_dice MonsterHitDice

The monster's Hit Dice, including the asterisk count.

required

Returns:

Type Description
int

The XP award.

reaction_result

reaction_result(table: ReactionTable, total: int) -> ReactionResult

Resolve a 2d6 reaction total against the table.

Totals below 2 and above 12 clamp into the outer bands — the table's own "2 or less" / "12 or more" semantics, so a CHA-modified total of 1 or 14 is never out of range.

Parameters:

Name Type Description Default
table ReactionTable

The loaded reaction table.

required
total int

The modified 2d6 total.

required

Returns:

Type Description
ReactionResult

The reaction result.

thac0_for_hd

thac0_for_hd(count: int, *, bonus_modifier: bool = False) -> tuple[int, int]

Return the attack-matrix THAC0 (and attack bonus) for a monster's Hit Dice.

Bonus hit-point modifiers attack as 1 HD higher; negative modifiers keep the unmodified row. Fractional and sub-1 HD use the "Up to 1" row (19 [0]). Monster stat blocks carry printed THAC0 — this lookup serves validation, custom monsters, and drained instances re-deriving from reduced HD.

Parameters:

Name Type Description Default
count int

The Hit Dice count.

required
bonus_modifier bool

True when the HD carry a positive hit-point modifier.

False

Returns:

Type Description
tuple[int, int]

The (thac0, attack_bonus) pair.

to_hit_ac

to_hit_ac(thac0: int, ac: int) -> int

Return the attack roll required to hit ac under the attack matrix.

Every printed matrix cell equals clamp(THAC0 − AC, 2, 20), and armour classes outside the printed −3..9 columns extend by the same formula: the printed bounds are page layout, not a rules cliff.

Parameters:

Name Type Description Default
thac0 int

The attacker's THAC0.

required
ac int

The defender's descending armour class.

required

Returns:

Type Description
int

The required roll, clamped to 2..20.

turning_column

turning_column(hit_dice: MonsterHitDice) -> str | None

Return the turning-table column for a monster's Hit Dice, or None above 9.

The column is the HD count as a string, with three exceptions: count 2 with a special ability (asterisks > 0) maps to 2* (the table's own footnote), counts 7–9 share 7-9, and counts above 9 have no column — turning fails against them. Modifiers don't shift columns (the mummy's 5+1 turns on column 5), and the asterisk matters only at count 2 (the wight's 3* is column 3).

Parameters:

Name Type Description Default
hit_dice MonsterHitDice

The monster's Hit Dice.

required

Returns:

Type Description
str | None

The column label, or None when the monster is beyond the printed table.

xp_band_label

xp_band_label(hit_dice: MonsterHitDice) -> str

Return the XP-awards row label for a monster's Hit Dice.

Negative hit-point modifiers map to the lower band — the goblin's 1-1 HD awards from the "Less than 1" row (the "attack as 1 HD higher" rule is for bonus modifiers only). Fractional and fixed-hp forms are "Less than 1". Above 21 HD every monster lands on the "21–21+" row and inflation applies (see monster_xp).

Parameters:

Name Type Description Default
hit_dice MonsterHitDice

The monster's Hit Dice.

required

Returns:

Type Description
str

The row label, e.g. "Less than 1", "2+", or "9–10+".