Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/front-ends/fastapi-pattern.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ Two details of that handler matter:

## The served content and its interpreter

The barrow is authored content: gated doors, a fetch quest, the works. Nothing plays that content until the [`Interpreter`][osrlib.crawl.interpreter.Interpreter] is registered on the session (see [Gates, triggers, and quests](../guides/gates-triggers-quests.md)). The server owns that wiring in `content.py`, and it happens on **both** entry paths, the fresh session and the restored one. A fresh session registers the interpreter the moment it is built:
The barrow is authored content: gated doors, a fetch quest, the works. Nothing plays that content until the [`Interpreter`][osrlib.crawl.interpreter.Interpreter] is registered on the session (see [Gates, triggers, and quests](../guides/gates-triggers-quests.md)). The server owns that setup in `content.py`, and it happens on **both** entry paths, the fresh session and the restored one. A fresh session registers the interpreter the moment it is built:

```{.python .no-run}
--8<-- "examples/fastapi_crawler/content.py:new-session"
Expand Down
2 changes: 1 addition & 1 deletion docs/front-ends/llm-referees.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ A practical narrator prompt sends the structured events (or their codes and fiel

An adventure written for the authored layer ([Gates, triggers, and quests](../guides/gates-triggers-quests.md)) arrives with material aimed squarely at a narrating referee, and an agent serving one should use all of it.

**Register the library's [`Interpreter`][osrlib.crawl.interpreter.Interpreter] and do no bookkeeping.** One `session.register_listener(Interpreter(session))` after the session is built (and again after a load), and the interpreter plays the triggers, the quests, the fired-marks, and the rewards as ordinary logged commands. The agent referees, and the interpreter runs the adventure's wiring.
**Register the library's [`Interpreter`][osrlib.crawl.interpreter.Interpreter] and do no bookkeeping.** One `session.register_listener(Interpreter(session))` after the session is built (and again after a load), and the interpreter plays the triggers, the quests, the fired-marks, and the rewards as ordinary logged commands. The agent referees, and the interpreter runs the adventure's setup.

**`guidance` is steering, never script.** [`NarrativeBlock`][osrlib.crawl.narrative.NarrativeBlock] has a `guidance` field on any authored object, and [`LevelSpec.guidance`][osrlib.crawl.dungeon.LevelSpec] holds whole-level ambience that hangs on no object at all. The TUI barrow's first level reads:

Expand Down
2 changes: 1 addition & 1 deletion docs/front-ends/tui-crawler.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ The event-level `Visibility` check above hides individual referee-only lines. Th

## The authored adventure

`content.py` builds the game's whole world: a town, a two-level barrow, and the errand that ends it, assembled from the same authoring models [Building an adventure](../getting-started/building-an-adventure.md) walks through. A keyed area binds content (descriptive text, an encounter, features) to a set of cells. The shrine below binds prose and the cache that holds the quest's MacGuffin, named by id so that taking it is something the quest can match on (the goblins are keyed to a different room):
`content.py` builds the game's whole world: a town, a two-level barrow, and the errand that ends it, assembled from the same authoring models [Building an adventure](../getting-started/building-an-adventure.md) walks through. A keyed area attaches content (descriptive text, an encounter, features) to a set of cells. The shrine below attaches prose and the cache that holds the quest's MacGuffin, named by id so that taking it is something the quest can match on (the goblins are keyed to a different room):

```{.python .no-run}
--8<-- "examples/tui_crawler/content.py:idol-shrine-area"
Expand Down
8 changes: 4 additions & 4 deletions docs/getting-started/building-an-adventure.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ An [`Edge`][osrlib.crawl.dungeon.Edge] is `open`, `wall`, or `door`. A door edge

## Keyed areas

Cells not covered by any area are corridor. An [`AreaSpec`][osrlib.crawl.dungeon.AreaSpec] names a region (a room, a cave, a shrine) and binds content to it: descriptive prose for your front end, an encounter, a trap, treasure. The party triggers an area's content by stepping into any of its cells:
Cells not covered by any area are corridor. An [`AreaSpec`][osrlib.crawl.dungeon.AreaSpec] names a region (a room, a cave, a shrine) and attaches content to it: descriptive prose for your front end, an encounter, a trap, treasure. The party triggers an area's content by stepping into any of its cells:

```{.python .no-run}
areas=(
Expand All @@ -48,7 +48,7 @@ Cells not covered by any area are corridor. An [`AreaSpec`][osrlib.crawl.dungeon
)
```

A [`KeyedEncounter`][osrlib.crawl.dungeon.KeyedEncounter] lists its monsters by template id, each with a fixed count or count dice. A template id is any id from [`load_monsters`][osrlib.data.load_monsters], listed in [the monster id index][monsters-index], or the id of a monster the adventure bundles (see [Bundling custom monsters with an adventure](../guides/authoring-custom-content.md#bundling-custom-monsters-with-an-adventure)). You can also pin the monsters' awareness, stance, or alignment.
A [`KeyedEncounter`][osrlib.crawl.dungeon.KeyedEncounter] lists its monsters by template id, each with a fixed count or count dice. A template id is any id from [`load_monsters`][osrlib.data.load_monsters], listed in [the monster id index][monsters-index], or the id of a monster the adventure bundles (see [Bundling custom monsters with an adventure](../guides/authoring-custom-content.md#bundling-custom-monsters-with-an-adventure)). You can also set the monsters' awareness, stance, or alignment.

Beyond encounters, an area (or the level itself) can contain:

Expand All @@ -60,7 +60,7 @@ Beyond encounters, an area (or the level itself) can contain:

## The dungeon, the town, and the root

You slot the level into a [`DungeonSpec`][osrlib.crawl.dungeon.DungeonSpec], and the dungeon into an [`Adventure`][osrlib.crawl.adventure.Adventure] beside the [`TownSpec`][osrlib.crawl.adventure.TownSpec]. The town is the safe base where the party rests, buys equipment, and sells treasure. `travel_turns` maps each dungeon id to the town-to-entrance travel cost in exploration turns:
You put the level into a [`DungeonSpec`][osrlib.crawl.dungeon.DungeonSpec], and the dungeon into an [`Adventure`][osrlib.crawl.adventure.Adventure] beside the [`TownSpec`][osrlib.crawl.adventure.TownSpec]. The town is the safe base where the party rests, buys equipment, and sells treasure. `travel_turns` maps each dungeon id to the town-to-entrance travel cost in exploration turns:

```{.python .no-run}
barrow = DungeonSpec(id="barrow", name="The Barrow", levels=(level,))
Expand Down Expand Up @@ -157,7 +157,7 @@ assert opened.accepted
result = session.execute(MoveParty(direction=Direction.EAST))
assert result.accepted

# Stepping into the keyed area spawns the goblins and starts an encounter.
# Stepping into the keyed area creates the goblins and starts an encounter.
assert session.mode is SessionMode.ENCOUNTER
assert len(session.monsters) == 2
```
Expand Down
16 changes: 8 additions & 8 deletions docs/guides/authoring-custom-content.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Authoring custom classes, spells, monsters, and items

A build pipeline compiles the OSE SRD into the package's data files: the classes, the spell list, the monster catalog, and the equipment lists that ship with osrlib. That pipeline isn't the extension point, and you can't feed your own content into it. What you can do is write your own class, spell, monster, and item definitions in code, validate them the same way the shipped catalogs validate their own content, and run them through the same kernel that plays a fighter or a goblin: creation, advancement, memorization, casting, and (for monsters) spawning, combat, XP, and treasure. A class's `race` and a spell's `spell_list` are both open, validated string ids for exactly this reason. Nothing in the kernel restricts them to the values the shipped classes use.
A build pipeline compiles the OSE SRD into the package's data files: the classes, the spell list, the monster catalog, and the equipment lists that ship with osrlib. That pipeline isn't the extension point, and you can't feed your own content into it. What you can do is write your own class, spell, monster, and item definitions in code, validate them the same way the shipped catalogs validate their own content, and run them through the same kernel that plays a fighter or a goblin: creation, advancement, memorization, casting, and (for monsters) creation, combat, XP, and treasure. A class's `race` and a spell's `spell_list` are both open, validated string ids for exactly this reason. Nothing in the kernel restricts them to the values the shipped classes use.

Start with one small custom class and one custom spell for it. The [complete program](#the-complete-program) runs every step shown along the way. After that come [bundling custom monsters with an adventure](#bundling-custom-monsters-with-an-adventure) and [bundling custom items with an adventure](#bundling-custom-items-with-an-adventure), both for the crawl layer. The scope here is the content catalogs, the *things* a game and its adventures can contain. Authored *behavior* is a different surface: the gated door, the trigger, and the quest are covered in [Gates, triggers, and quests](gates-triggers-quests.md).

Expand Down Expand Up @@ -134,11 +134,11 @@ assert warden.thac0 == 19
assert warden.saves.spells == 14
```

That's a plain Python module attribute, not a documented plugin point with its own function. If you want characters of a custom class, do the reassignment once, at startup, before you build or load any character, rather than calling it per character. `race` needs no such wiring. Both `ClassDefinition` and `Character` validate it against a slug pattern and nothing else, and no procedure looks it up anywhere, so any race string the two sides agree on already works.
That's a plain Python module attribute, not a documented plugin point with its own function. If you want characters of a custom class, do the reassignment once, at startup, before you build or load any character, rather than calling it per character. `race` needs no such setup. Both `ClassDefinition` and `Character` validate it against a slug pattern and nothing else, and no procedure looks it up anywhere, so any race string the two sides agree on already works.

## Advancing and casting

With the catalogs extended and the loader binding pointed at them, the rest of the lifecycle is the same kernel calls a shipped class goes through. `level_up` reads next level's row straight off `WARDEN`. `memorize_spells` checks the caster's list and slot capacity against the extended spell catalog. Casting consumes the memorized copy and resolves the mode's effect, which here heals a wounded ally by touch:
With the catalogs extended and the loader pointed at them, the rest of the lifecycle is the same kernel calls a shipped class goes through. `level_up` reads next level's row straight off `WARDEN`. `memorize_spells` checks the caster's list and slot capacity against the extended spell catalog. Casting consumes the memorized copy and resolves the mode's effect, which here heals a wounded ally by touch:

```{.python .no-run}
streams = RngStreams(master_seed=2026)
Expand Down Expand Up @@ -292,7 +292,7 @@ rejections = validate_class_choice(low_scores, WARDEN)
assert [rejection.code for rejection in rejections] == ["creation.class.requirements_not_met"]

# The one seam: Character.definition resolves load_classes() from this module's
# namespace, so a game holding custom definitions swaps that binding once, up front.
# namespace, so a game holding custom definitions reassigns it once, up front.
character_module.load_classes = lambda: classes

scores = {ability: 11 for ability in AbilityScore} | {AbilityScore.WIS: 13, AbilityScore.CON: 13}
Expand Down Expand Up @@ -353,7 +353,7 @@ assert warden.memorized_spells == ()

## Bundling custom monsters with an adventure

Monsters take a different transport than classes and spells, because the crawl layer already has a document that contains content: the adventure. `Adventure.monsters` bundles your own [`MonsterTemplate`][osrlib.core.monsters.MonsterTemplate]s with the adventure document, and every session running that adventure resolves them everywhere it resolves a shipped template id: keyed encounters, [`SpawnMonsters`][osrlib.crawl.commands.SpawnMonsters], inline wandering tables, listen checks, and [`GameSession.spawn`][osrlib.crawl.session.GameSession.spawn]. No loader reassignment, no registration. The document contains the content, and [`GameSession.effective_monsters`][osrlib.crawl.session.GameSession.effective_monsters] is the shipped catalog plus the bundle. Downstream of spawning, nothing is different for a bundled monster. A spawned [`MonsterInstance`][osrlib.core.monsters.MonsterInstance] embeds its full template, so combat, morale, XP, treasure, saves, and replay never look the id up again.
Monsters take a different transport than classes and spells, because the crawl layer already has a document that contains content: the adventure. `Adventure.monsters` bundles your own [`MonsterTemplate`][osrlib.core.monsters.MonsterTemplate]s with the adventure document, and every session running that adventure resolves them everywhere it resolves a shipped template id: keyed encounters, [`SpawnMonsters`][osrlib.crawl.commands.SpawnMonsters], inline wandering tables, listen checks, and [`GameSession.spawn`][osrlib.crawl.session.GameSession.spawn]. No loader reassignment, no registration. The document contains the content, and [`GameSession.effective_monsters`][osrlib.crawl.session.GameSession.effective_monsters] is the shipped catalog plus the bundle. Downstream of creation, nothing is different for a bundled monster. A created [`MonsterInstance`][osrlib.core.monsters.MonsterInstance] embeds its full template, so combat, morale, XP, treasure, saves, and replay never look the id up again.

A template is a frozen model you build with `model_validate`, exactly like the class and spell above. Three table helpers derive the stat-block numbers the SRD would print, so your creation matches the attack matrix, the monster save bands, and the XP awards table: [`thac0_for_hd`][osrlib.core.tables.thac0_for_hd], [`monster_save_band_label`][osrlib.core.tables.monster_save_band_label], and [`monster_xp`][osrlib.core.tables.monster_xp]. The one rule is the collision rule: a bundled id must not collide with the shipped catalog or another bundled id. [`validate_adventure`][osrlib.crawl.adventure.validate_adventure] rejects a collision outright and never overrides, so give a variant orc a variant id. [The monster id index][monsters-index] documents the shipped catalog only. Bundled ids live in the adventure that includes them:

Expand Down Expand Up @@ -431,7 +431,7 @@ hero = create_character(name="Hild", class_id="fighter", alignment=Alignment.LAW
session = GameSession.new(Party(members=[hero.character]), adventure, seed=7)

# The session's effective catalog resolves the bundled id — the very object the
# adventure carries — and spawning embeds it in each instance.
# adventure carries — and creation embeds it in each instance.
assert session.effective_monsters.get("bone_warden") is BONE_WARDEN
guards = session.spawn("bone_warden", 2)
assert [guard.template.id for guard in guards] == ["bone_warden", "bone_warden"]
Expand Down Expand Up @@ -549,15 +549,15 @@ Third, some class policies are written as id lists. A class whose [`WeaponPolicy

There's no merge path into the shipped content. `load_classes` and `load_spells` are cached loaders that read the generated `classes.json` and `spells.json` shipped inside the package. There's no append or register call, so an extended catalog is always a value your own code builds and keeps: `classes` and `spells` above, never something fed back into the loaders themselves. `load_monsters` and `load_equipment` are just as closed. Bundling ([monsters](#bundling-custom-monsters-with-an-adventure), [items](#bundling-custom-items-with-an-adventure)) joins your templates to the shipped catalog per session through the adventure document that contains them, and the shipped catalog objects never change.

[`create_character`][osrlib.core.character.create_character], the one-call wrapper used in [the quickstart](../getting-started/quickstart.md), resolves its `class_id` argument through `load_classes().get(class_id)`. That's the same module attribute `Character.definition` reads, so [the seam above](#the-one-seam-characters-of-a-custom-class) covers the wrapper too: reassign `load_classes` and `create_character(class_id="warden", ...)` rolls a warden. Leave the binding alone and the wrapper finds shipped ids only. Not one of the stepwise creation functions resolves a class by id, so none of them needs the seam: [`roll_ability_scores`][osrlib.core.character.roll_ability_scores], `validate_class_choice`, [`roll_hit_points`][osrlib.core.character.roll_hit_points], [`validate_extra_languages`][osrlib.core.character.validate_extra_languages], [`roll_starting_gold`][osrlib.core.character.roll_starting_gold], and [`choose_starting_spells`][osrlib.core.character.choose_starting_spells] take a `ClassDefinition` object or nothing but a stream, and they run the identical procedure `create_character` composes.
[`create_character`][osrlib.core.character.create_character], the one-call wrapper used in [the quickstart](../getting-started/quickstart.md), resolves its `class_id` argument through `load_classes().get(class_id)`. That's the same module attribute `Character.definition` reads, so [the seam above](#the-one-seam-characters-of-a-custom-class) covers the wrapper too: reassign `load_classes` and `create_character(class_id="warden", ...)` rolls a warden. Leave the assignment alone and the wrapper finds shipped ids only. Not one of the stepwise creation functions resolves a class by id, so none of them needs the seam: [`roll_ability_scores`][osrlib.core.character.roll_ability_scores], `validate_class_choice`, [`roll_hit_points`][osrlib.core.character.roll_hit_points], [`validate_extra_languages`][osrlib.core.character.validate_extra_languages], [`roll_starting_gold`][osrlib.core.character.roll_starting_gold], and [`choose_starting_spells`][osrlib.core.character.choose_starting_spells] take a `ClassDefinition` object or nothing but a stream, and they run the identical procedure `create_character` composes.

The `load_classes` reassignment above is a plain module attribute, not a supported extension API with its own function or parameter. There's nothing to call except swapping the name, and nothing checks that you swapped it back. If your game has custom classes, reassign it once at startup and keep your extended catalog as the only `load_classes` your characters ever see for the life of the process, the same way the [complete program](#the-complete-program) above does. Spells need no equivalent seam. Nothing resolves a spell by id off a character the way `Character.definition` resolves a class, so `SpellCatalog.get` and `SpellCatalog.by_list` calls against your own extended catalog are all a caster needs.

For the ids the shipped catalogs already use, see [the class id index][classes-index] and [the spell id index][spells-index]. For every model and function this page named, see [the API reference](../reference/api/index.md).

## Where next

- [Building an adventure](../getting-started/building-an-adventure.md) - the dungeon geometry and keyed content the bundled monsters and items above bind into.
- [Building an adventure](../getting-started/building-an-adventure.md) - the dungeon geometry and keyed content the bundled monsters and items above attach to.
- [Gates, triggers, and quests](gates-triggers-quests.md) - authored behavior: the gate that a bundled key opens, the trigger that matches a bundled id, the quest that ends the adventure.
- [Sessions, commands, and events](sessions-commands-events.md) - running a character, custom class or not, through an actual session once it exists.
- [The API reference](../reference/api/index.md) - the full model and function reference for everything named on this page.
Loading
Loading