B/X (1981 Basic/Expert) tabletop RPG rules engine for turn-based dungeon crawlers
Project description
osrlib
A Python library implementing the classic 1981 B/X (Basic/Expert) fantasy adventure game rules for turn-based, grid-based dungeon crawlers in the style of the original Bard's Tale. The rules are sourced from the Old-School Essentials System Reference Document, an Open Game Content restatement of the B/X rules. osrlib is the rules authority and game-state engine; your game supplies presentation, input, and content.
The library is headless and sans-I/O — it never renders, prompts, sleeps, or touches the network — and every game it runs is deterministic: the same seed and the same commands always replay the same game. Four kinds of consumer are first-class: a web or mobile backend (FastAPI over HTTP), a terminal game (a local TUI crawler), an LLM referee or narrator driven by structured events and typed commands, and scripts or simulations using the kernel à la carte.
Status: released — osrlib on PyPI. The public API is frozen, and the documentation site is the place to learn the library — quickstart, guides, front-end walk-throughs, and a full reference for every command, event, rejection code, and content id.
Installation
Requires Python ≥ 3.14. The only runtime dependency is pydantic.
uv add osrlib
or, with pip:
pip install osrlib
Quickstart
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
The documentation site walks this example step by step, then builds out from it: building an adventure, the session and event loop, and complete front-end walk-throughs for the two example games in examples/.
Determinism
Determinism is a public API guarantee. All randomness flows through named PCG64 streams forked from a master seed, so the same seed and the same key always produce the same stream — independently of any other stream:
from osrlib.core.dice import roll
from osrlib.core.rng import RngStreams
streams_a = RngStreams(master_seed=42)
streams_b = RngStreams(master_seed=42)
rolls_a = [roll("2d6×10", streams_a.get("treasure")).total for _ in range(3)]
rolls_b = [roll("2d6×10", streams_b.get("treasure")).total for _ in range(3)]
assert rolls_a == rolls_b # same seed + same key → identical sequences
Successive rolls on one stream differ, of course; reproducibility across derivations is the contract. Saved games replay from the seed and the command log, so a loaded game is bit-for-bit the game you saved.
SRD data pipeline
The game data in src/osrlib/data/ is generated from the scraped SRD markdown in srd/ and is never hand-edited. Regenerate it with:
uv run python -m tools.srd_compile
CI regenerates the data and fails on any diff, so srd/, the compiler, and the generated data cannot silently drift. Parser corrections belong in tools/srd_compile/overrides/, never in the output; every override carries a reason and is recorded in the output entry's overrides_applied provenance list. Rules interpretations and adaptations are documented in the adaptations register.
Contributing
Requires Python ≥ 3.14 and uv. Install from source and run the checks the way CI does:
git clone https://github.com/mmacy/osrlib-python.git
cd osrlib-python
uv sync
uv run ruff format --check
uv run ruff check
uv run pyright
uv run pytest
uv run mkdocs build --strict
The design is documented in the specification: architecture, contracts, rules scope, and the phased roadmap.
Licensing
This repository contains two kinds of material under two licenses:
- Library code is licensed under the MIT license.
- SRD-derived content — the scraped SRD text in
srd/and the compiled game data insrc/osrlib/data/— is Open Game Content used under the Open Game License 1.0a, which includes the complete Section 15 copyright notice. The data package ships its own copy of the license, with the osrlib Section 15 entry, inside the built wheel.
osrlib is an independent project, not affiliated with or endorsed by Necrotic Gnome. "Old-School Essentials" is a trademark of Necrotic Gnome, used here only to identify the source document; no claim of compatibility is made.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file osrlib-1.1.0.tar.gz.
File metadata
- Download URL: osrlib-1.1.0.tar.gz
- Upload date:
- Size: 401.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
685aacf02653074d4b5189f69579f4b784f7cb537fdeca14339f8d17f04aef72
|
|
| MD5 |
08579ffaeccf3f0b387bb3877eafc69f
|
|
| BLAKE2b-256 |
cc92e7b5242776bfb9fd1191557485350eb4a9f71364403ac65f4c33982d69da
|
Provenance
The following attestation bundles were made for osrlib-1.1.0.tar.gz:
Publisher:
release.yml on mmacy/osrlib-python
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
osrlib-1.1.0.tar.gz -
Subject digest:
685aacf02653074d4b5189f69579f4b784f7cb537fdeca14339f8d17f04aef72 - Sigstore transparency entry: 2083163665
- Sigstore integration time:
-
Permalink:
mmacy/osrlib-python@30d6c31f4e60c837aff3fbd89d7dc7665ac29e55 -
Branch / Tag:
refs/tags/v1.1.0 - Owner: https://github.com/mmacy
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@30d6c31f4e60c837aff3fbd89d7dc7665ac29e55 -
Trigger Event:
push
-
Statement type:
File details
Details for the file osrlib-1.1.0-py3-none-any.whl.
File metadata
- Download URL: osrlib-1.1.0-py3-none-any.whl
- Upload date:
- Size: 425.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2e8e36cebe241872a9bdbf0b7a194adf6bc893162b5342b3694fa9a55865e4cd
|
|
| MD5 |
2e3e1a5faf277e1a14be30ac82a448a4
|
|
| BLAKE2b-256 |
6f631df4b3355b92c862760f127ea3644598e0e24f54c69a804d2e2842f0d037
|
Provenance
The following attestation bundles were made for osrlib-1.1.0-py3-none-any.whl:
Publisher:
release.yml on mmacy/osrlib-python
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
osrlib-1.1.0-py3-none-any.whl -
Subject digest:
2e8e36cebe241872a9bdbf0b7a194adf6bc893162b5342b3694fa9a55865e4cd - Sigstore transparency entry: 2083163679
- Sigstore integration time:
-
Permalink:
mmacy/osrlib-python@30d6c31f4e60c837aff3fbd89d7dc7665ac29e55 -
Branch / Tag:
refs/tags/v1.1.0 - Owner: https://github.com/mmacy
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@30d6c31f4e60c837aff3fbd89d7dc7665ac29e55 -
Trigger Event:
push
-
Statement type: