osrlib.core.classes
Class definitions, level progression, XP awards, and leveling up.
The seven Classic classes compile from the OSE SRD's class pages into
ClassDefinition models, loaded frozen via
load_classes. A definition is pure data: requirements,
prime requisites, XP-modifier tiers, a per-level progression table (hit dice, THAC0,
saves, and spell-slot capacity), armour and weapon policies, and structured ability
tags — so an Advanced class is additive data rather than a code change. race is an
open, validated identity string, populated from the class in Classic play — no rules
procedure consumes it, so a new race is data too.
XP-modifier tiers are one uniform representation for every class: ordered tiers of
{modifier_pct, minimum scores}, evaluated best-first — the first tier whose minimum
scores all hold wins. osrlib reads this as the standard single-prime-requisite table's
intended behavior: its penalty rows only make sense under first-match evaluation. Elf
and halfling carry exactly their stated bonus tiers, which per RAW include no penalty
rows, as a documented adaptation (see the adaptations register).
Saves, THAC0, and spell slots are always read from the progression row for the
character's level, never stored as separate derived fields —
ClassDefinition.row is the pure
recompute-from-level lookup whose inverse is energy drain
(drain_levels).
Advancement lives here too: apply_xp awards XP and
levels a character up when a threshold is crossed,
level_up performs the level-up roll directly, and
drain_levels reverses it. Character creation
itself lives in osrlib.core.character.
PERCENTILE_THIEF_SKILLS
module-attribute
PERCENTILE_THIEF_SKILLS = (
"climb_sheer_surfaces",
"find_remove_treasure_traps",
"hide_in_shadows",
"move_silently",
"open_locks",
"pick_pockets",
)
The six d% roll-under thief skills; hear_noise is the seventh, rolled on 1d6.
ArmourPolicy
ArmourPolicyKind
Bases: StrEnum
What armour a class may wear.
ClassAbility
Bases: BaseModel
A structured class-ability tag plus the SRD prose it came from.
params carries the mechanizable numbers ({"range_feet": 60} for infravision);
manual marks abilities that need referee judgment and stay prose. The combat,
magic, and crawl procedures consume these tags.
ClassCatalog
Bases: BaseModel
The loaded class list, with id lookup.
get
get(class_id: str) -> ClassDefinition
Return the class definition for class_id.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
class_id
|
str
|
The class id to look up, e.g. |
required |
Returns:
| Type | Description |
|---|---|
ClassDefinition
|
The class definition. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If no class has that id. |
ClassDefinition
Bases: BaseModel
A character class, compiled from its SRD page.
Frozen SRD data: play never mutates a class definition. race is an open,
validated identity string — no rules procedure consumes it (racial mechanics
resolve through structured ability tags), so Advanced races are additive data,
not code. requirements are minimum scores checked at class choice;
may_not_lower carries adjustment-step restrictions (the thief's STR).
level_titles[i] is the title at level i + 1; the SRD's title lists run only
through name level, so they are shorter than the progression and levels beyond
them have no title entry.
row
row(level: int) -> ProgressionRow
Return the progression row for level — the pure recompute-from-level lookup.
Saves, THAC0, attack bonus, and spell slots are read from here, never stored: energy drain is this function's inverse, not a redesign.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
level
|
int
|
The character level, 1 through the class maximum. |
required |
Returns:
| Type | Description |
|---|---|
ProgressionRow
|
The progression row. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
DetectionResult
Bases: BaseModel
An X-in-6 detection check's outcome.
roll is None for a zero chance: no die is consumed, since there is nothing to
roll under — for example, a non-dwarf searching for construction tricks simply
fails without a roll.
DrainResult
Bases: BaseModel
The outcome of energy drain.
hp_rolls are the raw hit dice rolled for the drained levels (empty above name
level, where the loss is the flat-bonus delta). slain marks the terminal case:
a person drained of all levels dies, and spawn_consequence carries the SRD's
spawn prose as a structured-but-manual field — the kernel kills, the game
narrates.
HitDice
Bases: BaseModel
A progression row's Hit Dice: count dice of die sides plus a flat bonus.
Above name level the SRD notates flat bonuses with an asterisk (9d8+2*) meaning
CON modifiers no longer apply — the asterisk is data, carried as con_applies.
LevelUpResult
Bases: BaseModel
The outcome of gaining one level.
While HD count still grows, hp_roll is the raw die (CON applies when
con_applied, minimum 1 hp gained); above name level the gain is the flat-bonus
delta with no roll and no CON.
ProgressionRow
Bases: BaseModel
One level of a class progression table, exactly as the SRD prints it.
THAC0 is dual-format in the SRD (19 [0]); both the descending value and the
bracketed attack bonus are carried. spell_slots[i] is the number of memorizable
spells of spell level i + 1; the tuple is empty for non-casters.
SavingThrows
Bases: BaseModel
The five save values: death/poison, wands, paralysis/petrify, breath, spells/rods/staves.
SkillCheckResult
Bases: BaseModel
A thief skill check's outcome.
chance is the effective target after modifiers (the pick-pockets ≥1%-failure
cap applied). noticed is set only for pick pockets: a roll of more than twice
the effective chance means the attempted theft is noticed (RAW) — what the
victim does about it is a game procedure.
ThiefSkillRow
Bases: BaseModel
One level of the thief skill table.
Skills are d% roll-under percentages except hear_noise, an X-in-6 upper bound
(the SRD's 1–2 is stored as 2). Pick pockets can exceed 100 at high level; the
over-100 arithmetic belongs to the skill-check procedure.
find_remove_treasure_traps
class-attribute
instance-attribute
WeaponPolicy
Bases: BaseModel
A class's weapon policy.
weapon_ids is the explicit allow list (cleric: the five blunt weapons) or the
forbidden list (dwarf and halfling: long_bow, two_handed_sword), and is empty
for any. manual_notes keeps referee-judgment stature prose (the dwarf's "small
or normal sized", the halfling's "appropriate to stature") that cannot be
mechanized. The policy governs the weapons list only; gear combat facets are exempt
(see validate_equip).
WeaponPolicyKind
Bases: StrEnum
How a class's weapon list is expressed.
XpAwardResult
Bases: BaseModel
The outcome of applying an XP award.
modified_award is the award after the class XP-modifier percentage, floored.
clamped reports the one-level-per-award rule firing: XP that would reach two or
more levels above the starting level stops at 1 XP below the second level's
threshold.
XpTier
apply_xp
apply_xp(character: Character, definition: ClassDefinition, award: int, stream: RngStream) -> XpAwardResult
Apply an XP award: class modifier, the one-level-per-award rule, and leveling.
The class XP-modifier percentage applies first, with the result floored. Then the rule exactly as written: XP that would reach two or more levels above the starting level is clamped to 1 XP below the second level's threshold, and the character gains one level. At the class's maximum level no further levels are gained but XP keeps accumulating, unclamped — there is no next threshold to hold the character under.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
character
|
Character
|
The character receiving the award; mutated in place. |
required |
definition
|
ClassDefinition
|
The character's class definition. |
required |
award
|
int
|
The unmodified XP award. Non-negative. |
required |
stream
|
RngStream
|
The RNG stream for a level-up hit die roll. |
required |
Returns:
| Type | Description |
|---|---|
XpAwardResult
|
The award outcome, including the level-up result when one occurred. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the definition doesn't match the character's class or the award is negative. |
detection_chance
detection_chance(character: Character, definition: ClassDefinition, kind: str) -> int
Resolve a character's X-in-6 chance for one detection kind.
Precedence, applied in this order: listening uses the thief's hear_noise row
when present, else the class's listening_at_doors param, else the universal
1-in-6; secret doors use detect_secret_doors (elf 2) else 1; room traps use
detect_room_traps (dwarf 2) else 1; construction tricks use
detect_construction_tricks (dwarf 2) else zero — the OSE SRD grants the
perception to dwarves alone, and "as a dwarf you can sense" has no baseline for
others, unlike the universal 1-in-6 search chances the SRD states for all PCs.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
character
|
Character
|
The detecting character. |
required |
definition
|
ClassDefinition
|
The character's class definition. |
required |
kind
|
str
|
One of |
required |
Returns:
| Type | Description |
|---|---|
int
|
The X-in-6 chance (0 means no chance at all). |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the kind is unknown. |
detection_check
detection_check(chance_in_six: int, *, stream: RngStream) -> DetectionResult
Roll the shared X-in-6 detection check: searching, listening, demi-human tags.
A zero (or negative) chance consumes no draw and fails: the OSE SRD grants construction-trick perception to dwarves alone, and a character without it has nothing to roll under.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
chance_in_six
|
int
|
The X-in-6 chance, from
|
required |
stream
|
RngStream
|
The RNG stream, conventionally the crawl's |
required |
Returns:
| Type | Description |
|---|---|
DetectionResult
|
The check outcome. |
drain_levels
drain_levels(
character: Character,
definition: ClassDefinition,
*,
levels: int = 1,
xp_policy: str,
stream: RngStream,
spawn_consequence: str | None = None
) -> DrainResult
Drain experience levels — the inverse of level_up.
Saves, THAC0, and spell slots need no reversal because they derive from
row; only stored state reverses.
Per level drained, mirroring level_up exactly in reverse: above name level
subtract the flat-bonus delta (no roll, no CON); otherwise roll the class hit die
plus the CON modifier (minimum 1 per die) and subtract it from max and current
hit points — rolling the lost die is osrlib's RAW-faithful reading of "loses one
Hit Die of hit points" that keeps the model stateless.
Floors: drain never reduces max HP below 1 or current HP below 1 while the
character retains a level — death by drain happens only by losing the last
level ("a person drained of all levels"), the terminal state. XP is set once
after all levels drain, by policy: halfway is the floored midpoint of the
former and new levels' thresholds (the wight); level_minimum is the new
level's threshold exactly (wraith, spectre, vampire).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
character
|
Character
|
The drained character; mutated in place. |
required |
definition
|
ClassDefinition
|
The character's class definition. |
required |
levels
|
int
|
How many levels the drain removes (the spectre and vampire drain two, applying the procedure twice). |
1
|
xp_policy
|
str
|
|
required |
stream
|
RngStream
|
The RNG stream for the lost hit die rolls, conventionally
|
required |
spawn_consequence
|
str | None
|
The monster's spawn prose, carried on the drain event. |
None
|
Returns:
| Type | Description |
|---|---|
DrainResult
|
The drain outcome, including the terminal death when all levels are lost. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the definition doesn't match the character's class, |
level_up
level_up(character: Character, definition: ClassDefinition, stream: RngStream) -> LevelUpResult
Advance a character one level, rolling hit points per the SRD.
While the HD count still grows, the gain is a new hit die roll plus the CON
modifier, minimum 1. Above name level the gain is the flat-bonus delta between the
progression rows — no roll, no CON. Both max and current hit points increase by
the gain: leveling adds hit points but heals no damage already taken. Saves,
THAC0, and spell slots are never stored — read them from
ClassDefinition.row.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
character
|
Character
|
The character to advance; mutated in place. |
required |
definition
|
ClassDefinition
|
The character's class definition. |
required |
stream
|
RngStream
|
The RNG stream for the hit die roll, conventionally
|
required |
Returns:
| Type | Description |
|---|---|
LevelUpResult
|
The level-up outcome, including the raw hit die roll if one was made. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the definition doesn't match the character's class, or the character is already at the class's maximum level. |
thief_skill_check
thief_skill_check(
character: Character, definition: ClassDefinition, skill: str, *, modifier_pct: int = 0, stream: RngStream
) -> SkillCheckResult
Roll one thief skill check — an à la carte plain result, no events.
The six percentile skills roll d% with success on a result less than or equal
to the level row's chance; hear_noise rolls 1d6 against its X-in-6 bound. The
crawl procedures emit the events (and hide the referee-rolled outcomes); this
function just resolves the dice.
Pick pockets, per its RAW bullet: the caller folds the victim's over-5th-level
penalty into modifier_pct (−5% per victim level above 5th — the kernel never
sees the victim), the effective chance caps at 99 ("always at least a 1% chance
of failure"), and a roll of more than twice the effective chance sets noticed.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
character
|
Character
|
The rolling thief. |
required |
definition
|
ClassDefinition
|
The character's class definition; must carry a thief skill table. |
required |
skill
|
str
|
A percentile skill name from
|
required |
modifier_pct
|
int
|
A percentage adjustment to the percentile chance (ignored for
|
0
|
stream
|
RngStream
|
The RNG stream, conventionally the crawl's |
required |
Returns:
| Type | Description |
|---|---|
SkillCheckResult
|
The check outcome. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the class has no thief skills or the skill name is unknown — gating who may attempt a skill is the caller's validation. |
xp_modifier_pct
xp_modifier_pct(definition: ClassDefinition, scores: dict[AbilityScore, int]) -> int
Return the class XP-modifier percentage for a score set.
Tiers are evaluated best-first; the first tier whose minimums all hold wins. osrlib reads this as the standard table's intended behavior — its penalty rows only make sense under first-match evaluation. With no matching tier the modifier is zero, which is how the multi-prime-requisite classes carry no penalties per RAW.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
definition
|
ClassDefinition
|
The class definition. |
required |
scores
|
dict[AbilityScore, int]
|
The character's final ability scores. |
required |
Returns:
| Type | Description |
|---|---|
int
|
The XP modifier as a signed percentage ( |