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.
or, with pip:
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
- Building an adventure - the dungeon itself: the grid and its edges, keyed areas, and the content that binds to those areas.
- Gates, triggers, and quests - the authored layer: a door that needs a key, a lever that opens a portcullis, an errand that ends the adventure.
- Sessions, commands, and events - the command loop in depth: modes, rejections, the event log.
- Determinism, saves, and replay - what the seed guarantees and how saves and replay reach the same state.
- The TUI crawler - a complete example game built on everything above.