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
ABILITY_ROLL_ORDER = (
AbilityScore.STR,
AbilityScore.INT,
AbilityScore.WIS,
AbilityScore.DEX,
AbilityScore.CON,
AbilityScore.CHA,
)
The draw order for rolling ability scores: the SRD's listing order, always.
ADVANCEMENT_STREAM
module-attribute
Stream key convention for in-play advancement draws: level-up hit point rolls.
CHARACTER_CREATION_STREAM
module-attribute
Stream key convention for creation draws: ability scores, first-level hp, starting gold.
AbilityScoreRolls
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.
inventory
class-attribute
instance-attribute
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.
initiative_modifier
property
initiative_modifier: int
DEX modifier to individual initiative (optional rule).
magic_save_modifier
property
magic_save_modifier: int
WIS modifier to saving throws versus magical effects.
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
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
to_document
from_document
classmethod
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
|
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
HitPointRoll
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 |
required |
definition
|
ClassDefinition
|
The character's class. |
required |
catalog
|
SpellCatalog
|
The loaded spell catalog. |
required |
spell_ids
|
Sequence[str]
|
The chosen spell ids, from
|
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 |
required |
alignment
|
Alignment
|
The chosen alignment. |
required |
ruleset
|
Ruleset
|
The ruleset in play. |
required |
stream
|
RngStream
|
The RNG stream for creation draws, conventionally
|
required |
adjustment
|
AbilityAdjustment | None
|
The optional ability score adjustment. |
None
|
starting_spell_ids
|
Sequence[str]
|
The arcane starting spell book: ids from
|
()
|
extra_languages
|
Sequence[str]
|
INT-granted extra language choices: ids from
|
()
|
purchases
|
Sequence[tuple[str, int]]
|
|
()
|
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
Load a party from a stamped document.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
document
|
Mapping[str, object]
|
A document produced by
|
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
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
|
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
|
required |
Returns:
| Type | Description |
|---|---|
list[Rejection]
|
Structured rejections; empty when the choice is legal. |