Skip to content

Quickstart

Roll characters, form a party, build the smallest possible adventure, start a session, execute commands, read the events, and round-trip the game through a save. That's the whole loop, and you run it once here. The complete program is at the end of the page, and every snippet along the way comes from it.

Install osrlib from PyPI with uv or pip. You'll need Python ≥ 3.14.

uv add osrlib

or, with pip:

pip install osrlib

Roll the party

Character creation follows the SRD's procedure: roll ability scores, choose a class, roll hit points and starting gold. create_character runs the whole procedure in one call. Every random draw in osrlib comes from a named stream forked from a master seed, so the same seed always produces the same characters:

# Roll two 1st-level characters; every random draw comes from a named, seeded stream.
rules = Ruleset()
creation = RngStreams(master_seed=7).get(CHARACTER_CREATION_STREAM)
fighter = create_character(name="Hild", class_id="fighter", alignment=Alignment.LAWFUL, ruleset=rules, stream=creation)
cleric = create_character(name="Osric", class_id="cleric", alignment=Alignment.LAWFUL, ruleset=rules, stream=creation)
party = Party(members=[fighter.character, cleric.character])

class_id takes any id from load_classes, listed in the class id index. The result bundles the created Character with the raw rolls, which is why you build the party from fighter.character.

Build the smallest adventure

An Adventure is a town plus one or more dungeons. A dungeon level is a grid of 10-foot cells, and every edge between two cells is a wall unless you declare it open or a door. The dungeon you build here is a single corridor, two cells joined west-east:

# The smallest adventure: a town and a one-corridor dungeon, two cells joined west-east.
crypt = DungeonSpec(
    id="crypt",
    name="The Old Crypt",
    levels=(LevelSpec(number=1, width=2, height=1, entrance=(0, 0), edges={"1,0:west": Edge(kind=EdgeKind.OPEN)}),),
)
town = TownSpec(name="Threshold", travel_turns={"crypt": 1})
adventure = Adventure(name="A First Delve", town=town, dungeons=(crypt,))

The edge key "1,0:west" names the west side of cell (1, 0), the boundary between the two cells. For the geometry and the content models in full, see Building an adventure.

Start the session and move

A GameSession starts in town. EnterDungeon places the party at the entrance and switches the session to exploring, the only mode where movement and door commands are legal:

# A session starts in town; entering the dungeon switches it to exploring.
session = GameSession.new(party, adventure, seed=7)
session.execute(EnterDungeon(dungeon_id="crypt"))
assert session.mode is SessionMode.EXPLORING

Game state changes only through commands, and every rules resolution comes back as typed events. A rejected command changes nothing. Rejection is a normal in-fiction outcome, not an exception (see the rejection code reference):

# Commands in, events out: every rules resolution is a typed event with a message code.
result = session.execute(MoveParty(direction=Direction.EAST))
assert result.accepted
lines = [format_message(event) for event in result.events]
assert lines  # every event formats to a default English line

Events have structured fields and a message code, never baked prose. format_message is the default English formatter, and your front end can supply its own (see the message code reference).

Save and load

The whole session serializes to a JSON-compatible dict. Loading restores the session from that state alone and re-executes nothing. Replay is the separate replay_game path, which rebuilds the same session by re-executing the command log from the same seed. Load and replay always land in the identical state, and that's the determinism guarantee (see Determinism, saves, and replay):

# The whole session round-trips through JSON: same seed, same commands, same game.
document = save_game(session)
restored = load_game(document)
assert save_game(restored) == document

The complete program

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
from osrlib.crawl.adventure import Adventure, TownSpec
from osrlib.crawl.commands import EnterDungeon, MoveParty, SessionMode
from osrlib.crawl.dungeon import Direction, DungeonSpec, Edge, EdgeKind, LevelSpec
from osrlib.crawl.party import Party
from osrlib.crawl.session import GameSession
from osrlib.messages import format_message
from osrlib.persistence import load_game, save_game

# Roll two 1st-level characters; every random draw comes from a named, seeded stream.
rules = Ruleset()
creation = RngStreams(master_seed=7).get(CHARACTER_CREATION_STREAM)
fighter = create_character(name="Hild", class_id="fighter", alignment=Alignment.LAWFUL, ruleset=rules, stream=creation)
cleric = create_character(name="Osric", class_id="cleric", alignment=Alignment.LAWFUL, ruleset=rules, stream=creation)
party = Party(members=[fighter.character, cleric.character])

# The smallest adventure: a town and a one-corridor dungeon, two cells joined west-east.
crypt = DungeonSpec(
    id="crypt",
    name="The Old Crypt",
    levels=(LevelSpec(number=1, width=2, height=1, entrance=(0, 0), edges={"1,0:west": Edge(kind=EdgeKind.OPEN)}),),
)
town = TownSpec(name="Threshold", travel_turns={"crypt": 1})
adventure = Adventure(name="A First Delve", town=town, dungeons=(crypt,))

# A session starts in town; entering the dungeon switches it to exploring.
session = GameSession.new(party, adventure, seed=7)
session.execute(EnterDungeon(dungeon_id="crypt"))
assert session.mode is SessionMode.EXPLORING

# Commands in, events out: every rules resolution is a typed event with a message code.
result = session.execute(MoveParty(direction=Direction.EAST))
assert result.accepted
lines = [format_message(event) for event in result.events]
assert lines  # every event formats to a default English line

# The whole session round-trips through JSON: same seed, same commands, same game.
document = save_game(session)
restored = load_game(document)
assert save_game(restored) == document

Where next