Skip to content

osrlib.core.character

The player character: model, stepwise creation, and stamped serialization.

Character is the player character model. Its derived values — modifiers, AC, movement rate, literacy, languages — are properties computed from stored state, never stored themselves, so they can never desync from it. Model validation is structural only (score ranges, level within class bounds, HP bounds); whether a creation step itself was legal (was the adjustment legal, were requirements met) is enforced by the creation functions at the time of the step, because a finished character cannot re-derive its own history.

Creation follows the OSE SRD's Creating a Character steps as a sequence of pure functions a caller drives one at a time — roll_ability_scores, validate_class_choice, apply_adjustment, roll_hit_points, validate_extra_languages, roll_starting_gold, and equipment purchase — each taking the caller's choice and returning a structured result (including the raw rolls, for display) or a list of rejections rather than raising. Character creation is out-of-fiction and pre-session: it emits no events; the first events belong to play. create_character drives the whole sequence in one call for scripts and tests that already know every choice upfront.

Advancement — leveling up, energy drain, and XP awards — lives in osrlib.core.classes, which also defines the played class.

Two module-level RNG stream keys are the convention every session adopts: CHARACTER_CREATION_STREAM for creation draws (ability scores, first-level hit points, starting gold) and ADVANCEMENT_STREAM for in-play level-up hit point rolls, kept separate so a creation-rules change never shifts advancement draws already recorded in a save.

ABILITY_ROLL_ORDER module-attribute

The draw order for rolling ability scores: the SRD's listing order, always.

ADVANCEMENT_STREAM module-attribute

ADVANCEMENT_STREAM = 'advancement'

Stream key convention for in-play advancement draws: level-up hit point rolls.

CHARACTER_CREATION_STREAM module-attribute

CHARACTER_CREATION_STREAM = 'character_creation'

Stream key convention for creation draws: ability scores, first-level hp, starting gold.

AbilityScoreRolls

Bases: BaseModel

The rolled score set, with each score's raw 3d6 kept for display.

scores instance-attribute

scores: dict[AbilityScore, int]

rolls instance-attribute

Character

Bases: BaseModel

A player character.

id defaults to None: entity IDs are session-scoped, assigned when the character joins a session. carrying_treasure is basic encumbrance's referee judgment, set by the game. spell_book holds spell ids (arcane casters only; tuple order is acquisition order) and memorized_spells the prepared copies (tuple order is memorization order and is load-bearing: casting consumes the first matching copy and drain forgets newest-first). Spell slots stay derived — definition.row(level).spell_slots, never stored — so leveling and drain recompute them for free.

id class-attribute instance-attribute

id: str | None = None

name class-attribute instance-attribute

name: str = Field(min_length=1)

class_id instance-attribute

class_id: str

race class-attribute instance-attribute

race: str = Field(pattern='^[a-z][a-z0-9_]*$')

level class-attribute instance-attribute

level: int = Field(ge=1)

xp class-attribute instance-attribute

xp: int = Field(ge=0)

scores instance-attribute

scores: dict[AbilityScore, int]

alignment instance-attribute

alignment: Alignment

extra_languages class-attribute instance-attribute

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

max_hp class-attribute instance-attribute

max_hp: int = Field(ge=1)

current_hp class-attribute instance-attribute

current_hp: int = Field(ge=0)

inventory class-attribute instance-attribute

inventory: Inventory = Field(default_factory=Inventory)

carrying_treasure class-attribute instance-attribute

carrying_treasure: bool = False

conditions class-attribute instance-attribute

conditions: tuple[ActiveCondition, ...] = ()

stat_modifiers class-attribute instance-attribute

stat_modifiers: tuple[ActiveModifier, ...] = ()

spell_book class-attribute instance-attribute

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

memorized_spells class-attribute instance-attribute

memorized_spells: tuple[MemorizedSpell, ...] = ()

definition property

definition: ClassDefinition

The character's class definition, from the loaded class catalog.

thac0 property

thac0: int

THAC0 from the progression row for the current level — derived, never stored.

attack_bonus property

attack_bonus: int

Ascending-AC attack bonus from the progression row — derived, never stored.

saves property

saves: SavingThrows

Saving throws from the progression row for the current level — derived, never stored.

melee_modifier property

melee_modifier: int

STR modifier to melee attack and damage rolls.

open_doors_chance property

open_doors_chance: int

STR-derived X-in-6 chance to force a stuck door.

missile_modifier property

missile_modifier: int

DEX modifier to missile attack rolls.

initiative_modifier property

initiative_modifier: int

DEX modifier to individual initiative (optional rule).

hit_point_modifier property

hit_point_modifier: int

CON modifier per Hit Die rolled.

magic_save_modifier property

magic_save_modifier: int

WIS modifier to saving throws versus magical effects.

npc_reaction_modifier property

npc_reaction_modifier: int

CHA modifier to NPC reactions.

literacy property

literacy: Literacy

INT-derived literacy in the character's native languages.

alignment_tongue property

alignment_tongue: str

The alignment language, derived from alignment so it can never desync.

Alignment tongues are not languages.json entries; the derived identifier is alignment_ plus the alignment wire value (alignment_lawful).

languages property

languages: tuple[str, ...]

Every language the character speaks.

The alignment tongue, then the class natives (Common first, per the class pages), then INT-granted extras.

armour_class property

armour_class: int

Descending AC: armour base (9 unarmoured), −1 per bonus, minus the DEX modifier.

armour_class_ascending property

armour_class_ascending: int

Ascending AC: armour base (10 unarmoured), +1 per bonus, plus the DEX modifier.

movement_rate

movement_rate(ruleset: Ruleset) -> int

Return the movement rate in feet per turn under the ruleset's encumbrance mode.

Parameters:

Name Type Description Default
ruleset Ruleset

The ruleset in play.

required

Returns:

Type Description
int

The movement rate: 120, 90, 60, 30, or 0.

to_document

to_document() -> dict[str, object]

Serialize to a stamped document with schema and engine versions.

Returns:

Type Description
dict[str, object]

The stamped document envelope wrapping the serialized character.

from_document classmethod

from_document(document: Mapping[str, object]) -> Character

Load a character from a stamped document.

Unknown payload fields are ignored, per the additive-schema contract.

Parameters:

Name Type Description Default
document Mapping[str, object]

A document produced by to_document.

required

Returns:

Type Description
Character

The reconstructed character.

Raises:

Type Description
ContentValidationError

If the envelope or payload is malformed or of the wrong kind.

SaveVersionError

If the document's schema version is newer than this library understands.

CharacterCreationResult

Bases: BaseModel

A created character plus the raw rolls creation consumed, for display.

character instance-attribute

character: Character

ability_rolls instance-attribute

ability_rolls: AbilityScoreRolls

hit_point_roll instance-attribute

hit_point_roll: HitPointRoll

gold_roll instance-attribute

gold_roll: RollResult

HitPointRoll

Bases: BaseModel

A first-level hit point roll: every raw die (re-rolls included) and the final total.

rolls instance-attribute

rolls: tuple[int, ...]

hit_points class-attribute instance-attribute

hit_points: int = Field(ge=1)

choose_starting_spells

choose_starting_spells(
    character: Character, definition: ClassDefinition, catalog: SpellCatalog, spell_ids: Sequence[str]
) -> list[Rejection]

Fill an arcane caster's starting spell book — the last creation step.

The stepwise creation surface: validates the choice (see validate_starting_spells) and a still-empty book, then writes it. Creation stays event-less like every other creation step.

Parameters:

Name Type Description Default
character Character

The created character; its spell_book is written.

required
definition ClassDefinition

The character's class.

required
catalog SpellCatalog

The loaded spell catalog.

required
spell_ids Sequence[str]

The chosen spell ids, from load_spells — see the spell id index.

required

Returns:

Type Description
list[Rejection]

Structured rejections; empty when the book was written.

create_character

create_character(
    *,
    name: str,
    class_id: str,
    alignment: Alignment,
    ruleset: Ruleset,
    stream: RngStream,
    adjustment: AbilityAdjustment | None = None,
    starting_spell_ids: Sequence[str] = (),
    extra_languages: Sequence[str] = (),
    purchases: Sequence[tuple[str, int]] = (),
    equip_ids: Sequence[str] = ()
) -> CharacterCreationResult

Create a 1st-level character with all decisions supplied upfront.

A convenience for scripts and tests: it calls the same stepwise creation functions used for interactive play, in the SRD's order — roll scores, validate the class choice, adjust scores, choose the spell book (the SRD's step 6, before hit points; it consumes no draws), roll hit points, validate languages, roll starting gold, buy and equip — drawing scores, hit points, and gold from stream in that fixed order.

Parameters:

Name Type Description Default
name str

The character's name.

required
class_id str

The chosen class id, from load_classes — see the class id index.

required
alignment Alignment

The chosen alignment.

required
ruleset Ruleset

The ruleset in play.

required
stream RngStream

The RNG stream for creation draws, conventionally CHARACTER_CREATION_STREAM.

required
adjustment AbilityAdjustment | None

The optional ability score adjustment.

None
starting_spell_ids Sequence[str]

The arcane starting spell book: ids from load_spells — see the spell id index. Must be exactly the level-1 memorization capacity (one first-level spell for magic-user and elf).

()
extra_languages Sequence[str]

INT-granted extra language choices: ids from load_languages — see the language id index.

()
purchases Sequence[tuple[str, int]]

(item_id, lots) pairs bought in order from the starting gold. item_id is from load_equipment — see the equipment index. lots is how many of the catalog's unit of sale to buy: weapons and armour sell one at a time, so lots is the quantity bought, while gear and ammunition sell in fixed-size lots (one lot of torches is six torches).

()
equip_ids Sequence[str]

Item ids to equip after purchase, in order.

()

Returns:

Type Description
CharacterCreationResult

The created character and the raw creation rolls.

Raises:

Type Description
ValueError

If any decision is illegal for the rolled scores — unknown ids, a failed class requirement, an illegal adjustment, spell choices, language choices, an unaffordable purchase, or a forbidden equip. Callers wanting structured reasons drive the stepwise functions themselves.

Examples:

from osrlib.core.alignment import Alignment
from osrlib.core.character import CHARACTER_CREATION_STREAM, create_character
from osrlib.core.rng import RngStreams
from osrlib.core.ruleset import Ruleset

streams = RngStreams(master_seed=2)
stream = streams.get(CHARACTER_CREATION_STREAM)
result = create_character(
    name="Rurik",
    class_id="fighter",
    alignment=Alignment.LAWFUL,
    ruleset=Ruleset(),
    stream=stream,
    purchases=[("sword", 1), ("leather", 1)],
    equip_ids=["sword", "leather"],
)
character = result.character
assert character.class_id == "fighter"
assert character.level == 1
assert character.max_hp == character.current_hp == 8
assert character.armour_class == 6
assert character.inventory.worn_armour.template.id == "leather"
assert character.inventory.purse.gp == 30

party_from_document

party_from_document(document: Mapping[str, object]) -> list[Character]

Load a party from a stamped document.

Parameters:

Name Type Description Default
document Mapping[str, object]

A document produced by party_to_document.

required

Returns:

Type Description
list[Character]

The reconstructed characters, in order.

Raises:

Type Description
ContentValidationError

If the envelope or payload is malformed or of the wrong kind.

SaveVersionError

If the document's schema version is newer than this library understands.

party_to_document

party_to_document(characters: Sequence[Character]) -> dict[str, object]

Serialize a party — a stamped collection of characters.

The crawl-layer party model (marching order, shared resources) is Party; this is the kernel's party-as-collection.

Parameters:

Name Type Description Default
characters Sequence[Character]

The party members, in order.

required

Returns:

Type Description
dict[str, object]

The stamped document envelope wrapping the serialized characters.

roll_ability_scores

roll_ability_scores(stream: RngStream) -> AbilityScoreRolls

Roll 3d6 for each ability, drawn in the SRD's order STR INT WIS DEX CON CHA.

The draw order is fixed: it's part of the determinism contract for the CHARACTER_CREATION_STREAM convention.

Parameters:

Name Type Description Default
stream RngStream

The RNG stream to draw from.

required

Returns:

Type Description
AbilityScoreRolls

The rolled scores and each score's individual dice.

roll_hit_points

roll_hit_points(definition: ClassDefinition, con_modifier: int, ruleset: Ruleset, stream: RngStream) -> HitPointRoll

Roll first-level hit points: the class hit die plus the CON modifier, minimum 1.

With the hp_reroll_at_first_level flag on, the die is re-rolled while the raw die shows 1–2 (before the CON modifier), each re-roll consuming a draw — osrlib's reading of the SRD's "re-rolling 1s and 2s".

Parameters:

Name Type Description Default
definition ClassDefinition

The character's class.

required
con_modifier int

The CON hit point modifier for the (adjusted) scores.

required
ruleset Ruleset

The ruleset in play.

required
stream RngStream

The RNG stream to draw from.

required

Returns:

Type Description
HitPointRoll

Every raw die rolled and the final hit point total.

roll_starting_gold

roll_starting_gold(stream: RngStream) -> RollResult

Roll starting money: 3d6 × 10 gold pieces, via the dice grammar.

Parameters:

Name Type Description Default
stream RngStream

The RNG stream to draw from.

required

Returns:

Type Description
RollResult

The roll, whose total is the starting gold in gp.

validate_class_choice

validate_class_choice(scores: dict[AbilityScore, int], definition: ClassDefinition) -> list[Rejection]

Validate a class choice against the class's minimum score requirements.

Parameters:

Name Type Description Default
scores dict[AbilityScore, int]

The rolled scores. Requirements are checked before adjustment, mirroring the SRD's step order (choose class, then adjust). For Classic data the adjustment step can never break them — every requirement minimum is 9, only STR, INT, and WIS may be lowered, and never below 9 — but an Advanced class with a higher minimum on a lowerable non-prime ability would need a re-check after adjustment.

required
definition ClassDefinition

The chosen class.

required

Returns:

Type Description
list[Rejection]

Structured rejections; empty when the choice is legal.

validate_extra_languages

validate_extra_languages(definition: ClassDefinition, int_score: int, choices: Sequence[str]) -> list[Rejection]

Validate INT-granted extra language choices.

Extras must come from the SRD's Other Languages table (the twenty choosable languages), may not duplicate a class native, may not repeat, and may not exceed the INT table's additional-languages allowance.

Parameters:

Name Type Description Default
definition ClassDefinition

The chosen class, whose natives the choices may not duplicate.

required
int_score int

The character's (adjusted) INT score.

required
choices Sequence[str]

The chosen extra language ids, from load_languages — see the language id index.

required

Returns:

Type Description
list[Rejection]

Structured rejections; empty when the choices are legal.

validate_starting_spells

validate_starting_spells(
    definition: ClassDefinition, catalog: SpellCatalog, spell_ids: Sequence[str]
) -> list[Rejection]

Validate a starting spell-book choice against class and capacity rules.

Arcane casters "begin play with as many spells in their spell book as they are able to memorize" (the OSE SRD's spell books rules) — the per-level counts must equal the level-1 slot counts exactly, which for both magic-user and elf means one first-level spell. The caller supplies the choice: "The referee may choose these spells or may allow the player to select" — the game owns the decision, the kernel validates it. Clerics (and non-casters) start with nothing: any selection for them is rejected.

Parameters:

Name Type Description Default
definition ClassDefinition

The character's class.

required
catalog SpellCatalog

The loaded spell catalog.

required
spell_ids Sequence[str]

The chosen spell ids, from load_spells — see the spell id index.

required

Returns:

Type Description
list[Rejection]

Structured rejections; empty when the choice is legal.