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
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
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.
CombatTables
Bases: BaseModel
The loaded combat tables.
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. |
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. |
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).
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.
EncounterTables
Bases: BaseModel
The six dungeon encounter tables plus the NPC adventurer generation tables.
npc_class_levels
class-attribute
instance-attribute
npc_class_levels: tuple[NpcClassLevelRow, ...] = ()
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 |
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.
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.
NpcAlignmentBand
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.
NpcPartyComposition
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.
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.
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.
ReactionTable
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).
TurningRow
TurningTable
Bases: BaseModel
The turning-undead table: 11 cleric-level rows (1–10, 11+) × 8 HD columns.
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
|
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
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: |
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
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 |
to_hit_ac
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 |
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. |